├── config ├── test.exs └── config.exs ├── .ackrc ├── .formatter.exs ├── test ├── test_helper.exs ├── support │ ├── test_display.ex │ ├── test_accessory_server.ex │ ├── test_value_store.ex │ └── test_http_client.ex └── hap │ ├── tlv_encoder_test.exs │ ├── iid_test.exs │ ├── pair_verify_test.exs │ ├── identify_test.exs │ ├── tlv_parser_test.exs │ ├── hap_session_transport_test.exs │ ├── pair_setup_test.exs │ └── accessories_test.exs ├── .github ├── dependabot.yml └── workflows │ ├── rerun.yml │ └── elixir.yml ├── lib ├── hap │ ├── characteristics │ │ ├── on.ex │ │ ├── identify.ex │ │ ├── hold_position.ex │ │ ├── name.ex │ │ ├── outlet_in_use.ex │ │ ├── status_active.ex │ │ ├── model.ex │ │ ├── motion_detected.ex │ │ ├── firmware_revision.ex │ │ ├── version.ex │ │ ├── obstruction_detected.ex │ │ ├── manufacturer.ex │ │ ├── serial_number.ex │ │ ├── pm10_density.ex │ │ ├── voc_density.ex │ │ ├── ozone_density.ex │ │ ├── pm2_5_density.ex │ │ ├── active.ex │ │ ├── service_label_index.ex │ │ ├── sulphur_dioxide_density.ex │ │ ├── carbon_monoxide_level.ex │ │ ├── nitrogen_dioxide_density.ex │ │ ├── carbon_dioxide_level.ex │ │ ├── carbon_dioxide_peak_level.ex │ │ ├── carbon_monoxide_peak_level.ex │ │ ├── mute.ex │ │ ├── hue.ex │ │ ├── target_temperature.ex │ │ ├── volume.ex │ │ ├── brightness.ex │ │ ├── water_level.ex │ │ ├── saturation.ex │ │ ├── target_relative_humidity.ex │ │ ├── current_ambient_light_level.ex │ │ ├── current_position.ex │ │ ├── current_tilt_angle.ex │ │ ├── rotation_speed.ex │ │ ├── target_position.ex │ │ ├── target_tilt_angle.ex │ │ ├── current_temperature.ex │ │ ├── slat_type.ex │ │ ├── current_relative_humidity.ex │ │ ├── current_vertical_tilt_angle.ex │ │ ├── current_horizontal_tilt_angle.ex │ │ ├── target_vertical_tilt_angle.ex │ │ ├── swing_mode.ex │ │ ├── target_fan_state.ex │ │ ├── target_horizontal_tilt_angle.ex │ │ ├── leak_detected.ex │ │ ├── smoke_detected.ex │ │ ├── cooling_threshold_temperature.ex │ │ ├── heating_threshold_temperature.ex │ │ ├── current_fan_state.ex │ │ ├── current_slat_state.ex │ │ ├── lock_target_state.ex │ │ ├── temperature_display_units.ex │ │ ├── occupancy_detected.ex │ │ ├── target_current_air_purifier_state.ex │ │ ├── rotation_direction.ex │ │ ├── air_quality.ex │ │ ├── current_air_purifier_state.ex │ │ ├── current_heating_cooling_state.ex │ │ ├── identifier.ex │ │ ├── lock_physical_controls.ex │ │ ├── lock_current_state.ex │ │ ├── current_heater_cooler_state.ex │ │ ├── carbon_dioxide_detected.ex │ │ ├── contact_sensor_state.ex │ │ ├── target_door_state.ex │ │ ├── carbon_monoxide_detected.ex │ │ ├── is_configured.ex │ │ ├── service_label_namespace.ex │ │ ├── active_identifier.ex │ │ ├── volume_selector.ex │ │ ├── configured_name.ex │ │ ├── position_state.ex │ │ ├── target_media_state.ex │ │ ├── current_visibility_state.ex │ │ ├── closed_captions.ex │ │ ├── current_media_state.ex │ │ ├── target_visibility_state.ex │ │ ├── display_order.ex │ │ ├── status_fault.ex │ │ ├── status_low_battery.ex │ │ ├── volume_control_type.ex │ │ ├── input_device_type.ex │ │ ├── power_mode_selection.ex │ │ ├── sleep_discovery_mode.ex │ │ ├── color_temperature.ex │ │ ├── picture_mode.ex │ │ ├── status_tampered.ex │ │ ├── input_source_type.ex │ │ ├── current_door_state.ex │ │ ├── target_heater_cooler_state.ex │ │ ├── remote_key.ex │ │ └── input-event.ex │ ├── crypto │ │ ├── sha512.ex │ │ ├── ecdh.ex │ │ ├── hkdf.ex │ │ ├── eddsa.ex │ │ ├── cha_cha_20.ex │ │ └── srp6a.ex │ ├── services │ │ ├── protocol_information.ex │ │ ├── switch.ex │ │ ├── service_label.ex │ │ ├── speaker.ex │ │ ├── faucet.ex │ │ ├── microphone.ex │ │ ├── outlet.ex │ │ ├── door_bell.ex │ │ ├── stateless_programmable_switch.ex │ │ ├── light_bulb.ex │ │ ├── leak_sensor.ex │ │ ├── contact_sensor.ex │ │ ├── motion_sensor.ex │ │ ├── occupancy_sensor.ex │ │ ├── light_sensor.ex │ │ ├── smoke_sensor.ex │ │ ├── temperature_sensor.ex │ │ ├── humidity_sensor.ex │ │ ├── slat.ex │ │ ├── fan_v2.ex │ │ ├── door.ex │ │ ├── window.ex │ │ ├── television_speaker.ex │ │ ├── accessory_information.ex │ │ ├── garage_door.ex │ │ ├── carbon_dioxide_sensor.ex │ │ ├── carbon_monoxide_sensor.ex │ │ ├── air_purifier.ex │ │ ├── air_quality_sensor.ex │ │ ├── window_covering.ex │ │ ├── input_source.ex │ │ ├── heater_cooler.ex │ │ ├── thermostat.ex │ │ └── television.ex │ ├── service_source.ex │ ├── tlv_encoder.ex │ ├── console_display.ex │ ├── http_server.ex │ ├── event_manager.ex │ ├── kino_display.ex │ ├── discovery.ex │ ├── hap_session_handler.ex │ ├── tlv_parser.ex │ ├── service.ex │ ├── persistent_storage.ex │ ├── display.ex │ ├── cleartext_http_server.ex │ ├── iid.ex │ ├── accessory.ex │ ├── characteristic_definition.ex │ ├── encrypted_http_server.ex │ ├── value_store.ex │ ├── characteristic.ex │ ├── pair_verify.ex │ ├── pairings.ex │ ├── hap_session_transport.ex │ ├── accessory_server_manager.ex │ └── pair_setup.ex └── hap.ex ├── .gitignore ├── LICENSE └── README.md /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :mdns_lite, skip_udp: true 4 | -------------------------------------------------------------------------------- /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-file=is:report_file_test.xml 2 | --ignore-dir=is:deps 3 | --ignore-dir=is:log 4 | --ignore-dir=is:doc 5 | --ignore-dir=is:.elixir_ls 6 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | line_length: 120, 3 | inputs: [ 4 | "{lib,config,test}/**/*.{ex,exs}", 5 | "apps/*/mix.exs", 6 | "mix.exs" 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Path.wildcard(Path.join(__DIR__, "support/*.{ex,exs}")) |> Enum.each(&Code.require_file/1) 2 | ExUnit.start() 3 | Logger.configure(level: :warning) 4 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | env_config = Path.expand("#{Mix.env()}.exs", __DIR__) 4 | 5 | if File.exists?(env_config) do 6 | import_config(env_config) 7 | end 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "mix" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /lib/hap/characteristics/on.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.On do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.on` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "25" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/identify.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Identify do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.identify` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "14" 9 | def perms, do: ["pw"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/hold_position.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.HoldPosition do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.position.hold` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "6F" 9 | def perms, do: ["pw"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/name.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Name do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.name` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "23" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | def max_length, do: 64 12 | end 13 | -------------------------------------------------------------------------------- /lib/hap/characteristics/outlet_in_use.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.OutletInUse do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.outlet-in-use` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "26" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/status_active.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.StatusActive do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.status-active` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "75" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/model.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Model do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.model` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "21" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | def max_length, do: 64 12 | end 13 | -------------------------------------------------------------------------------- /lib/hap/characteristics/motion_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.MotionDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.motion-detected` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "22" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/firmware_revision.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.FirmwareRevision do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.firmware.revision` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "52" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/version.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Version do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.version` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "37" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | def max_length, do: 64 12 | end 13 | -------------------------------------------------------------------------------- /lib/hap/characteristics/obstruction_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ObstructionDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.obstruction-detected` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "24" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "bool" 11 | end 12 | -------------------------------------------------------------------------------- /lib/hap/characteristics/manufacturer.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Manufacturer do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.manufacturer` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "20" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | def max_length, do: 64 12 | end 13 | -------------------------------------------------------------------------------- /lib/hap/characteristics/serial_number.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SerialNumber do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.serial-number` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "30" 9 | def perms, do: ["pr"] 10 | def format, do: "string" 11 | def max_length, do: 64 12 | end 13 | -------------------------------------------------------------------------------- /lib/hap/characteristics/pm10_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.PM10Density do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.pm10` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C7" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/voc_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.VOCDensity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.voc` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C8" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/ozone_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.OzoneDensity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.ozone` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C3" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/pm2_5_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.PM25Density do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.pm2_5` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C6" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/active.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Active do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.active` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "B0" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "uint8" 11 | def min_value, do: 0 12 | def max_value, do: 1 13 | def step_value, do: 1 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/characteristics/service_label_index.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ServiceLabelIndex do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.service-label-index` characteristic 4 | 5 | """ 6 | 7 | @behaviour HAP.CharacteristicDefinition 8 | 9 | def type, do: "CB" 10 | def perms, do: ["pr"] 11 | def format, do: "uint8" 12 | def min_value, do: 1 13 | def step_value, do: 1 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/characteristics/sulphur_dioxide_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SulphurDioxideDensity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.so2` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C5" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_monoxide_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonMonoxideLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-monoxide.level` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "90" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/nitrogen_dioxide_density.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.NitrogenDioxideDensity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.density.no2` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C4" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 1000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_dioxide_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonDioxideLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-dioxide.level` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "93" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100_000 13 | end 14 | -------------------------------------------------------------------------------- /test/support/test_display.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Test.Display do 2 | @moduledoc false 3 | 4 | @behaviour HAP.Display 5 | 6 | require Logger 7 | 8 | @impl HAP.Display 9 | def display_pairing_code(_name, _pairing_code, _pairing_url), do: :ok 10 | 11 | @impl HAP.Display 12 | def clear_pairing_code, do: :ok 13 | 14 | @impl HAP.Display 15 | def identify(name) do 16 | Logger.warning("IDENTIFY #{name}") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_dioxide_peak_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonDioxidePeakLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-dioxide.peak-level` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "94" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100_000 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_monoxide_peak_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonMonoxidePeakLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-monoxide.peak-level` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "91" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | end 14 | -------------------------------------------------------------------------------- /lib/hap/characteristics/mute.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Mute do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.mute` characteristic 4 | # Valid Values 5 | # 6 | # 0 ”Mute is Off / Audio is On” 7 | # 1 ”Mute is On / There is no Audio” 8 | 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "11A" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "bool" 16 | end 17 | -------------------------------------------------------------------------------- /lib/hap/characteristics/hue.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Hue do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.hue` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "13" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 360.0 13 | def step_value, do: 1.0 14 | def unit, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_temperature.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetTemperature do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.temperature.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "35" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 10 12 | def max_value, do: 38 13 | def step_value, do: 0.1 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/characteristics/volume.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Volume do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.volume` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "119" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "uint8" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/crypto/sha512.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.SHA512 do 2 | @moduledoc false 3 | # Simple wrapper around Erlang's `:crypto.hash()` function 4 | 5 | @type message :: binary() 6 | @type hash :: binary() 7 | 8 | @doc """ 9 | Returns the SHA-512 has of the given message. 10 | 11 | Returns the hash directly (not contained in a success tuple) 12 | """ 13 | @spec hash(message()) :: hash() 14 | def hash(x), do: :crypto.hash(:sha512, x) 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/brightness.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Brightness do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.brightness` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "8" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "int" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/water_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.WaterLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.water-level` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "B5" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 100.0 13 | def step_value, do: 1.0 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/saturation.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Saturation do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.saturation` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "2F" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 100.0 13 | def step_value, do: 1.0 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_relative_humidity.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetRelativeHumidity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.relative.humidity.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "34" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | end 15 | -------------------------------------------------------------------------------- /test/support/test_accessory_server.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Test.TestAccessoryServer do 2 | @moduledoc false 3 | 4 | def test_server(config \\ []) do 5 | accessory_server = 6 | %HAP.AccessoryServer{ 7 | identifier: "11:22:33:44:55:66", 8 | display_module: HAP.Test.Display, 9 | data_path: Temp.mkdir!() 10 | } 11 | |> HAP.AccessoryServer.compile() 12 | |> struct(config) 13 | 14 | {HAP, accessory_server} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_ambient_light_level.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentAmbientLightLevel do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.light-level.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "6B" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0001 12 | def max_value, do: 100_000 13 | def units, do: "lux" 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_position.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentPosition do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.position.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "6D" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "uint8" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.tilt.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C1" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/rotation_speed.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.RotationSpeed do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.rotation.speed` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "29" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 100.0 13 | def step_value, do: 1.0 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_position.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetPosition do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.position.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "7C" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "uint8" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.tilt.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "C2" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_temperature.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentTemperature do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.temperature.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "11" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 100.0 13 | def step_value, do: 0.1 14 | def unit, do: "celsius" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/slat_type.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SlatType do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.type.slat` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Horizontal 8 | 1 Vertical 9 | 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "C0" 15 | def perms, do: ["pr"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_relative_humidity.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentRelativeHumidity do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.relative-humidity.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "10" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0 12 | def max_value, do: 100 13 | def step_value, do: 1 14 | def unit, do: "percentage" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_vertical_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentVerticalTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.vertical-tilt.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "6E" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_horizontal_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentHorizontalTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.horizontal-tilt.current` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "6C" 9 | def perms, do: ["pr", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_vertical_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetVerticalTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.vertical-tilt.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "7D" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/services/protocol_information.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.ProtocolInformation do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.protocol.information.service` service 4 | """ 5 | 6 | defstruct [] 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(_value) do 10 | %HAP.Service{ 11 | type: "A2", 12 | characteristics: [ 13 | {HAP.Characteristics.Version, "1.1.0"} 14 | ] 15 | } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/rerun.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | run_id: 5 | required: true 6 | jobs: 7 | rerun: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: rerun ${{ inputs.run_id }} 11 | env: 12 | GH_REPO: ${{ github.repository }} 13 | GH_TOKEN: ${{ github.token }} 14 | GH_DEBUG: api 15 | run: | 16 | gh run watch ${{ inputs.run_id }} > /dev/null 2>&1 17 | gh run rerun ${{ inputs.run_id }} --failed 18 | -------------------------------------------------------------------------------- /lib/hap/characteristics/swing_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SwingMode do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.swing-mode` characteristic 4 | 5 | Valid values: 6 | 7 | 0: Swing disabled 8 | 1: Swing enabled 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "B6" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_fan_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetFanState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.fan.state.target` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Manual 8 | 1 Auto 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "BF" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_horizontal_tilt_angle.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetHorizontalTiltAngle do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.horizontal-tilt.target` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "7B" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "int" 11 | def min_value, do: -90 12 | def max_value, do: 90 13 | def step_value, do: 1 14 | def units, do: "arcdegrees" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/leak_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.LeakDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.leak-detected` characteristic 4 | 5 | Valid values: 6 | 0: Leak is not detected 7 | 1: Leak is detected 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "70" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/smoke_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SmokeDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.smoke-detected` characteristic 4 | 5 | Valid values: 6 | 0: Smoke is not detected 7 | 1: Smoke is detected 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "76" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/cooling_threshold_temperature.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CoolingThresholdTemperature do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.temperature.cooling-threshold` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "D" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 10.0 12 | def max_value, do: 35.0 13 | def step_value, do: 0.1 14 | def units, do: "celsius" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/characteristics/heating_threshold_temperature.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.HeatingThresholdTemperature do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.temperature.heating-threshold` characteristic 4 | """ 5 | 6 | @behaviour HAP.CharacteristicDefinition 7 | 8 | def type, do: "12" 9 | def perms, do: ["pr", "pw", "ev"] 10 | def format, do: "float" 11 | def min_value, do: 0.0 12 | def max_value, do: 25.0 13 | def step_value, do: 0.1 14 | def units, do: "celsius" 15 | end 16 | -------------------------------------------------------------------------------- /test/hap/tlv_encoder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.TLVEncoderTest do 2 | use ExUnit.Case 3 | 4 | test "encodes properly" do 5 | expected = 6 | <<1, 255>> <> 7 | :binary.copy(<<0xAA>>, 255) <> 8 | <<1, 255>> <> :binary.copy(<<0xAB>>, 255) <> <<1, 10>> <> :binary.copy(<<0xBA>>, 10) <> <<2, 1, 3>> 9 | 10 | data = :binary.copy(<<0xAA>>, 255) <> :binary.copy(<<0xAB>>, 255) <> :binary.copy(<<0xBA>>, 10) 11 | 12 | assert HAP.TLVEncoder.to_binary(%{1 => data, 2 => <<3>>}) == expected 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_fan_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentFanState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.fan.state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Inactive 8 | 1 Idle 9 | 2 Blowing Air 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "AF" 15 | def perms, do: ["pr", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 2 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_slat_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentSlatState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.slat.state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Fixed 8 | 1 Jammed 9 | 2 Swinging 10 | 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "AA" 16 | def perms, do: ["pr", "ev"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 2 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/characteristics/lock_target_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.LockTargetState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.lock-mechanism.target-state` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Unsecured 8 | 1 Secured 9 | 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "1E" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/temperature_display_units.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TemperatureDisplayUnits do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.temperature.units` characteristic 4 | 5 | Valid values: 6 | 7 | 0: Celsius 8 | 1: Fahrenheit 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "36" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /lib/hap/characteristics/occupancy_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.OccupancyDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.occupancy-detected` characteristic 4 | 5 | Valid values: 6 | 0: Occupancy is not detected 7 | 1: Occupancy is detected 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "71" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_current_air_purifier_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetAirPurifierState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.air-purifier.state.target` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Manual 8 | 1 Auto 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "A8" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /lib/hap/characteristics/rotation_direction.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.RotationDirection do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.rotation.direction` characteristic 4 | 5 | Valid values 6 | 7 | 0 Clockwise 8 | 1 Counter-clockwise 9 | 2-255 Reserved 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "28" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "int" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/air_quality.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.AirQuality do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.air-quality` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Unknown 8 | 1 Excellent 9 | 2 Good 10 | 3 Fair 11 | 4 Inferior 12 | 5 Poor 13 | """ 14 | 15 | @behaviour HAP.CharacteristicDefinition 16 | 17 | def type, do: "95" 18 | def perms, do: ["pr", "ev"] 19 | def format, do: "uint8" 20 | def min_value, do: 0 21 | def max_value, do: 5 22 | def step_value, do: 1 23 | end 24 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_air_purifier_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentAirPurifierState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.air-purifier.state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Inactive 8 | 1 Idle 9 | 2 Purifying Air 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "A9" 15 | def perms, do: ["pr", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 2 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_heating_cooling_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentHeatingCoolingState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.heating-cooling.state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Off 8 | 1 Heating 9 | 2 Cooling 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "F" 15 | def perms, do: ["pr", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 2 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/identifier.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.Identifier do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.identifier` characteristic 4 | 5 | A unique identifier for the input source. This value is used to reference 6 | this input source from the ActiveIdentifier characteristic of the Television 7 | service. 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "E6" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint32" 15 | def min_value, do: 0 16 | end 17 | -------------------------------------------------------------------------------- /lib/hap/characteristics/lock_physical_controls.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.LockPhysicalControls do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.lock-physical-controls` characteristic 4 | 5 | Valid values: 6 | 7 | 0: Control lock disabled 8 | 1: Control lock enabled 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "A7" 14 | def perms, do: ["pr", "pw", "ev"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /lib/hap/characteristics/lock_current_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.LockCurrentState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.lock-mechanism.current-state` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Unsecured 8 | 1 Secured 9 | 2 Jammed 10 | 3 Unknowm 11 | 12 | """ 13 | 14 | @behaviour HAP.CharacteristicDefinition 15 | 16 | def type, do: "1D" 17 | def perms, do: ["pr", "ev"] 18 | def format, do: "uint8" 19 | def min_value, do: 0 20 | def max_value, do: 3 21 | def step_value, do: 1 22 | end 23 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_heater_cooler_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentHeaterCoolerState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.heater-cooler.state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Inactive 8 | 1 Idle 9 | 2 Heating 10 | 3 Cooling 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "B1" 16 | def perms, do: ["pr", "ev"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 3 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_dioxide_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonDioxideDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-dioxide.detected` characteristic 4 | 5 | Valid values: 6 | 0: Carbon Dioxide levels are normal 7 | 1: Carbon Dioxide levels are abnormal 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "92" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/contact_sensor_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ContactSensorState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.contact-state` characteristic 4 | 5 | A value of 0 indicates that the contact is detected. A value of 1 indicates 6 | that the contact is not detected. 7 | """ 8 | 9 | @behaviour HAP.CharacteristicDefinition 10 | 11 | def type, do: "6A" 12 | def perms, do: ["pr", "ev"] 13 | def format, do: "uint8" 14 | def min_value, do: 0 15 | def max_value, do: 1 16 | def step_value, do: 1 17 | end 18 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_door_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetDoorState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.door-state.target` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Open - The door is fully open 8 | 1 Closed - The door is fully closed 9 | 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "32" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/carbon_monoxide_detected.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CarbonMonoxideDetected do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.carbon-monoxide.detected` characteristic 4 | 5 | Valid values: 6 | 0: Carbon Monoxide levels are normal 7 | 1: Carbon Monoxide levels are abnormal 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "69" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/is_configured.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.IsConfigured do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.is-configured` characteristic 4 | 5 | Valid values: 6 | 0 - Not Configured 7 | 1 - Configured 8 | 9 | Indicates whether this input source has been configured. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "D6" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/service_label_namespace.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ServiceLabelNamespace do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.service-label-namespace` characteristic 4 | 5 | Valid Values 6 | 0 ”Dots. For e.g ”.” ”..” ”...” ”....”” 7 | 1 ”Arabic numerals. For e.g. 0,1,2,3” 8 | 9 | """ 10 | 11 | @behaviour HAP.CharacteristicDefinition 12 | 13 | def type, do: "CD" 14 | def perms, do: ["pr"] 15 | def format, do: "uint8" 16 | def min_value, do: 0 17 | def max_value, do: 1 18 | def step_value, do: 1 19 | end 20 | -------------------------------------------------------------------------------- /test/hap/iid_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.IIDTest do 2 | use ExUnit.Case 3 | 4 | test "encodes & decodes service records properly" do 5 | assert HAP.IID.to_iid(123) |> HAP.IID.service_index() == {:ok, 123} 6 | end 7 | 8 | test "encodes service record 0 as iid 1" do 9 | assert HAP.IID.to_iid(123, 234) |> HAP.IID.service_index() == {:ok, 123} 10 | assert HAP.IID.to_iid(123, 234) |> HAP.IID.characteristic_index() == {:ok, 234} 11 | end 12 | 13 | test "encodes & decodes characteristic records properly" do 14 | assert HAP.IID.to_iid(0) == 1 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hap/characteristics/active_identifier.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ActiveIdentifier do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.active-identifier` characteristic 4 | 5 | This characteristic is used to represent the currently active input source 6 | on the television. The value corresponds to the identifier of the selected 7 | input source. 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "E7" 13 | def perms, do: ["pr", "pw", "ev"] 14 | def format, do: "uint32" 15 | def min_value, do: 0 16 | end 17 | -------------------------------------------------------------------------------- /lib/hap/characteristics/volume_selector.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.VolumeSelector do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.volume-selector` characteristic 4 | 5 | Valid values: 6 | 0 - Increment 7 | 1 - Decrement 8 | 9 | This is a write-only characteristic used to adjust volume in increments. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "EA" 15 | def perms, do: ["pw"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/configured_name.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ConfiguredName do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.configured-name` characteristic 4 | 5 | This characteristic allows the user to provide a custom name for the accessory 6 | or service. This is typically used for input sources on a television to allow 7 | custom naming like "Cable Box" or "Game Console". 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "E3" 13 | def perms, do: ["pr", "pw", "ev"] 14 | def format, do: "string" 15 | end 16 | -------------------------------------------------------------------------------- /lib/hap/services/switch.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Switch do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.switch` service 4 | """ 5 | 6 | defstruct on: nil, name: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "on", value.on) 11 | 12 | %HAP.Service{ 13 | type: "49", 14 | characteristics: [ 15 | {HAP.Characteristics.On, value.on}, 16 | {HAP.Characteristics.Name, value.name} 17 | ] 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/characteristics/position_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.PositionState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.position.state` characteristic 4 | 5 | Valid values: 6 | 0: Going to the minimum value specified in metadata 7 | 1: Going to the maximum value specified in metadata 8 | 2: Stopped 9 | 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "72" 15 | def perms, do: ["pr", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 2 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_media_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetMediaState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.target-media-state` characteristic 4 | 5 | Valid values: 6 | 0 - Play 7 | 1 - Pause 8 | 2 - Stop 9 | 10 | Used to control the playback state of media on the television. 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "137" 16 | def perms, do: ["pr", "pw", "ev"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 2 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_visibility_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentVisibilityState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.current-visibility-state` characteristic 4 | 5 | Valid values: 6 | 0 - Shown 7 | 1 - Hidden 8 | 9 | Indicates whether this input source is currently shown or hidden in the UI. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "135" 15 | def perms, do: ["pr", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/closed_captions.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ClosedCaptions do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.closed-captions` characteristic 4 | 5 | Valid values: 6 | 0 - Closed Captions Disabled 7 | 1 - Closed Captions Enabled 8 | 9 | Controls whether closed captions are displayed on the television. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "DD" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_media_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentMediaState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.current-media-state` characteristic 4 | 5 | Valid values: 6 | 0 - Play 7 | 1 - Pause 8 | 2 - Stop 9 | 3 - Unknown 10 | 11 | Represents the current playback state of media on the television. 12 | """ 13 | 14 | @behaviour HAP.CharacteristicDefinition 15 | 16 | def type, do: "E0" 17 | def perms, do: ["pr", "ev"] 18 | def format, do: "uint8" 19 | def min_value, do: 0 20 | def max_value, do: 3 21 | def step_value, do: 1 22 | end 23 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_visibility_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetVisibilityState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.target-visibility-state` characteristic 4 | 5 | Valid values: 6 | 0 - Shown 7 | 1 - Hidden 8 | 9 | Used to control whether this input source should be shown or hidden in the UI. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "134" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "uint8" 17 | def min_value, do: 0 18 | def max_value, do: 1 19 | def step_value, do: 1 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/display_order.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.DisplayOrder do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.display-order` characteristic 4 | 5 | Represents the display order of input sources. This is used to determine 6 | the order in which input sources are displayed in the Home app. 7 | 8 | The value is a base64-encoded TLV8 structure containing the ordered 9 | identifiers of the input sources. 10 | """ 11 | 12 | @behaviour HAP.CharacteristicDefinition 13 | 14 | def type, do: "136" 15 | def perms, do: ["pr", "pw", "ev"] 16 | def format, do: "tlv8" 17 | end 18 | -------------------------------------------------------------------------------- /lib/hap/characteristics/status_fault.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.StatusFault do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.status-fault` characteristic 4 | 5 | A non-zero value indicates that the accessory has experienced a fault that 6 | may be interfering with its intended functionality. A value of 0 indicates 7 | that there is no fault. 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "77" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/status_low_battery.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.StatusLowBattery do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.status-lo-batt` characteristic 4 | 5 | A status of 1 indicates that the battery level of the accessory is low. 6 | Value should return to 0 when the battery charges to a level that's above the 7 | low threshold. 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "79" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/volume_control_type.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.VolumeControlType do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.volume-control-type` characteristic 4 | 5 | Valid values: 6 | 0 - None 7 | 1 - Relative 8 | 2 - Relative with Current 9 | 3 - Absolute 10 | 11 | Specifies the type of volume control supported by the television speaker. 12 | """ 13 | 14 | @behaviour HAP.CharacteristicDefinition 15 | 16 | def type, do: "E9" 17 | def perms, do: ["pr", "ev"] 18 | def format, do: "uint8" 19 | def min_value, do: 0 20 | def max_value, do: 3 21 | def step_value, do: 1 22 | end 23 | -------------------------------------------------------------------------------- /lib/hap/characteristics/input_device_type.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.InputDeviceType do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.input-device-type` characteristic 4 | 5 | Valid values: 6 | 0 - Other 7 | 1 - TV 8 | 2 - Recording 9 | 3 - Tuner 10 | 4 - Playback 11 | 5 - Audio System 12 | 13 | Specifies the type of device connected to this input source. 14 | """ 15 | 16 | @behaviour HAP.CharacteristicDefinition 17 | 18 | def type, do: "DC" 19 | def perms, do: ["pr", "ev"] 20 | def format, do: "uint8" 21 | def min_value, do: 0 22 | def max_value, do: 5 23 | def step_value, do: 1 24 | end 25 | -------------------------------------------------------------------------------- /lib/hap/service_source.ex: -------------------------------------------------------------------------------- 1 | defprotocol HAP.ServiceSource do 2 | @moduledoc """ 3 | A protocol which allows for arbitrary service definitions to compile themselves into `HAP.Service` structs 4 | for use within HAP. This protocol allows HAP to expose pre-defined services such as `HAP.Services.Lightbulb` 5 | with fields reflecting the domain of the service, while allowing HAP to work internally with a service tree 6 | close to that defined in the HomeKit specification 7 | """ 8 | 9 | @doc """ 10 | Compile the given value into a `HAP.Service` struct 11 | """ 12 | @spec compile(t()) :: HAP.Service.t() 13 | def compile(_value) 14 | end 15 | -------------------------------------------------------------------------------- /lib/hap/services/service_label.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.ServiceLabel do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.service-label` service 4 | """ 5 | 6 | defstruct service_label_namespace: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "service_label_namespace", value.service_label_namespace) 11 | 12 | %HAP.Service{ 13 | type: "CC", 14 | characteristics: [ 15 | {HAP.Characteristics.ServiceLabelNamespace, value.service_label_namespace} 16 | ] 17 | } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/hap/characteristics/power_mode_selection.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.PowerModeSelection do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.power-mode-selection` characteristic 4 | 5 | Valid values: 6 | 0 - Show (View TV Settings) 7 | 1 - Hide (Hide TV Settings) 8 | 9 | This characteristic controls whether the television settings should be 10 | accessible in the Home app. 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "DF" 16 | def perms, do: ["pw"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 1 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/characteristics/sleep_discovery_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.SleepDiscoveryMode do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.sleep-discovery-mode` characteristic 4 | 5 | Valid values: 6 | 0 - Not Discoverable 7 | 1 - Always Discoverable 8 | 9 | This characteristic determines whether the television accessory should be 10 | discoverable while in sleep mode. 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "E8" 16 | def perms, do: ["pr", "ev"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 1 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /lib/hap/services/speaker.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Speaker do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.speaker` service 4 | """ 5 | 6 | defstruct mute: nil, name: nil, volume: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "mute", value.mute) 11 | 12 | %HAP.Service{ 13 | type: "113", 14 | characteristics: [ 15 | {HAP.Characteristics.Mute, value.mute}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.Volume, value.volume} 18 | ] 19 | } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | uses: mtrudel/elixir-ci-actions/.github/workflows/test.yml@main 12 | lint: 13 | uses: mtrudel/elixir-ci-actions/.github/workflows/lint.yml@main 14 | re-run: 15 | needs: [test, lint] 16 | if: failure() && fromJSON(github.run_attempt) < 3 17 | runs-on: ubuntu-latest 18 | steps: 19 | - env: 20 | GH_REPO: ${{ github.repository }} 21 | GH_TOKEN: ${{ github.token }} 22 | GH_DEBUG: api 23 | run: gh workflow run rerun.yml -F run_id=${{ github.run_id }} 24 | -------------------------------------------------------------------------------- /lib/hap/characteristics/color_temperature.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.ColorTemperature do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.color-temperature` characteristic 4 | 5 | This characteristic describes color temperature which is represented in reciprocal 6 | megaKelvin (MK-1) or mirek scale. (M = 1,000,000 / K where M is the desired mirek 7 | value and K is temperature in Kelvin) 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "CE" 13 | def perms, do: ["pr", "pw", "ev"] 14 | def format, do: "uint32" 15 | def min_value, do: 50 16 | def max_value, do: 400 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/characteristics/picture_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.PictureMode do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.picture-mode` characteristic 4 | 5 | Valid values: 6 | 0 - Other 7 | 1 - Standard 8 | 2 - Calibrated 9 | 3 - Calibrated Dark 10 | 4 - Vivid 11 | 5 - Game 12 | 6 - Computer 13 | 7 - Custom 14 | 15 | Controls the picture mode/preset of the television display. 16 | """ 17 | 18 | @behaviour HAP.CharacteristicDefinition 19 | 20 | def type, do: "E2" 21 | def perms, do: ["pr", "pw", "ev"] 22 | def format, do: "uint8" 23 | def min_value, do: 0 24 | def max_value, do: 13 25 | def step_value, do: 1 26 | end 27 | -------------------------------------------------------------------------------- /lib/hap/characteristics/status_tampered.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.StatusTampered do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.status-tampered` characteristic 4 | 5 | This characteristic describes an accessory which has been tampered with. 6 | A status of 1 indicates that the accessory has been tampered with. Value 7 | should return to 0 when the accessory has been reset to a non-tampered state. 8 | """ 9 | 10 | @behaviour HAP.CharacteristicDefinition 11 | 12 | def type, do: "7A" 13 | def perms, do: ["pr", "ev"] 14 | def format, do: "uint8" 15 | def min_value, do: 0 16 | def max_value, do: 1 17 | def step_value, do: 1 18 | end 19 | -------------------------------------------------------------------------------- /lib/hap/services/faucet.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Faucet do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.faucet` service 4 | """ 5 | 6 | defstruct active: nil, name: nil, fault: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "active", value.active) 11 | 12 | %HAP.Service{ 13 | type: "D7", 14 | characteristics: [ 15 | {HAP.Characteristics.Active, value.active}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusFault, value.fault} 18 | ] 19 | } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/hap/services/microphone.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Microphone do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.microphone` service 4 | """ 5 | 6 | defstruct mute: nil, name: nil, volume: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "mute", value.mute) 11 | 12 | %HAP.Service{ 13 | type: "112", 14 | characteristics: [ 15 | {HAP.Characteristics.Mute, value.mute}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.Volume, value.volume} 18 | ] 19 | } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hap-*.tar 24 | 25 | # Ignore dialyzer caches 26 | /priv/plts/ 27 | -------------------------------------------------------------------------------- /lib/hap/characteristics/input_source_type.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.InputSourceType do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.input-source-type` characteristic 4 | 5 | Valid values: 6 | 0 - Other 7 | 1 - Home Screen 8 | 2 - Tuner 9 | 3 - HDMI 10 | 4 - Composite Video 11 | 5 - S-Video 12 | 6 - Component Video 13 | 7 - DVI 14 | 8 - AirPlay 15 | 9 - USB 16 | 10 - Application 17 | 18 | Specifies the type of input source. 19 | """ 20 | 21 | @behaviour HAP.CharacteristicDefinition 22 | 23 | def type, do: "DB" 24 | def perms, do: ["pr", "ev"] 25 | def format, do: "uint8" 26 | def min_value, do: 0 27 | def max_value, do: 10 28 | def step_value, do: 1 29 | end 30 | -------------------------------------------------------------------------------- /lib/hap/characteristics/current_door_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.CurrentDoorState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.door-state.current` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Open - The door is fully open 8 | 1 Closed - The door is fully closed 9 | 2 Opening - The door is actively opening 10 | 3 Closing - The door is actively closing 11 | 4 Stopped - The door is not moving, and it is not fully open nor fully closed 12 | 13 | """ 14 | 15 | @behaviour HAP.CharacteristicDefinition 16 | 17 | def type, do: "E" 18 | def perms, do: ["pr", "ev"] 19 | def format, do: "uint8" 20 | def min_value, do: 0 21 | def max_value, do: 4 22 | def step_value, do: 1 23 | end 24 | -------------------------------------------------------------------------------- /lib/hap/services/outlet.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Outlet do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.outlet` service 4 | """ 5 | 6 | defstruct on: nil, outlet_in_use: nil, name: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "on", value.on) 11 | HAP.Service.ensure_required!(__MODULE__, "outlet_in_use", value.outlet_in_use) 12 | 13 | %HAP.Service{ 14 | type: "47", 15 | characteristics: [ 16 | {HAP.Characteristics.On, value.on}, 17 | {HAP.Characteristics.OutletInUse, value.outlet_in_use}, 18 | {HAP.Characteristics.Name, value.name} 19 | ] 20 | } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/hap/characteristics/target_heater_cooler_state.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.TargetHeaterCoolerState do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.heater-cooler.target` characteristic 4 | 5 | Valid values: 6 | 7 | 0 Off 8 | 1 Heat (if current temperature is below the target temperature then turn on heating) 9 | 2 Cooling (if current temperature is above the target temperature then turn on cooling) 10 | 3 Auto (turn on heating or cooling to maintain temperature within the target temperatures) 11 | """ 12 | 13 | @behaviour HAP.CharacteristicDefinition 14 | 15 | def type, do: "33" 16 | def perms, do: ["pr", "pw", "ev"] 17 | def format, do: "uint8" 18 | def min_value, do: 0 19 | def max_value, do: 3 20 | def step_value, do: 1 21 | end 22 | -------------------------------------------------------------------------------- /test/hap/pair_verify_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.PairVerifyTest do 2 | use ExUnit.Case, async: false 3 | 4 | setup do 5 | {:ok, _pid} = HAP.Test.TestAccessoryServer.test_server() |> start_supervised() 6 | 7 | :ok 8 | end 9 | 10 | test "a valid pair-verify flow results in a pairing being made" do 11 | # Build our request parameters 12 | port = HAP.AccessoryServerManager.port() 13 | {:ok, client} = HAP.Test.HTTPClient.init(:localhost, port) 14 | 15 | # Ensure that we are not encrypted to start 16 | refute HAP.Test.HTTPClient.encrypted_session?() 17 | 18 | # Setup an encrypted session 19 | :ok = HAP.Test.HTTPClient.setup_encrypted_session(client) 20 | 21 | # Finally, ensure that we're working with an encrypted session 22 | assert HAP.Test.HTTPClient.encrypted_session?() 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/hap/services/door_bell.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.DoorBell do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.doorbell` service 4 | """ 5 | 6 | defstruct input_event: nil, 7 | name: nil, 8 | volume: nil, 9 | brightness: nil 10 | 11 | defimpl HAP.ServiceSource do 12 | def compile(value) do 13 | HAP.Service.ensure_required!(__MODULE__, "input_event", value.input_event) 14 | 15 | %HAP.Service{ 16 | type: "121", 17 | characteristics: [ 18 | {HAP.Characteristics.InputEvent, value.input_event}, 19 | {HAP.Characteristics.Name, value.name}, 20 | {HAP.Characteristics.Volume, value.volume}, 21 | {HAP.Characteristics.Brightness, value.brightness} 22 | ] 23 | } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/hap/characteristics/remote_key.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.RemoteKey do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.remote-key` characteristic 4 | 5 | Valid values for Apple TV remote control: 6 | 0 - Rewind 7 | 1 - Fast Forward 8 | 2 - Next Track 9 | 3 - Previous Track 10 | 4 - Arrow Up 11 | 5 - Arrow Down 12 | 6 - Arrow Left 13 | 7 - Arrow Right 14 | 8 - Select 15 | 9 - Back 16 | 10 - Exit 17 | 11 - Play/Pause 18 | 15 - Information 19 | 20 | This is a write-only characteristic that receives remote control 21 | button presses from HomeKit. 22 | """ 23 | 24 | @behaviour HAP.CharacteristicDefinition 25 | 26 | def type, do: "E1" 27 | def perms, do: ["pw"] 28 | def format, do: "uint8" 29 | def min_value, do: 0 30 | def max_value, do: 16 31 | def step_value, do: 1 32 | end 33 | -------------------------------------------------------------------------------- /lib/hap/tlv_encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.TLVEncoder do 2 | @moduledoc false 3 | # Provides functions to encode a map or keyword list into a TLV binary as described 4 | # in Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 5 | 6 | @doc """ 7 | Converts the provided map or keyword list into a TLV binary 8 | """ 9 | def to_binary(tlv) do 10 | tlv 11 | |> Enum.flat_map(&to_single_tlv/1) 12 | |> Enum.join() 13 | end 14 | 15 | defp to_single_tlv({tag, value}) do 16 | case value do 17 | <> -> 18 | [<> | to_single_tlv({tag, rest})] 19 | 20 | <<>> -> 21 | [] 22 | 23 | <> -> 24 | [<>] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hap/characteristics/input-event.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristics.InputEvent do 2 | @moduledoc """ 3 | Definition of the `public.hap.characteristic.input-event` characteristic 4 | 5 | Valid values: 6 | 0 ”Single Press” 7 | 1 ”Double Press” 8 | 2 ”Long Press” 9 | 10 | NOTE specification requirement: 11 | For IP accessories, the accessory must set the value of Paired Read to null(i.e. ”value” : null) in the attribute database. A read of this characteristic must always return a null value for IP accessories. 12 | 13 | The value must only be reported in the events (”ev”) property. 14 | """ 15 | 16 | @behaviour HAP.CharacteristicDefinition 17 | 18 | def type, do: "73" 19 | def perms, do: ["pr", "ev"] 20 | def format, do: "uint8" 21 | def min_value, do: 0 22 | def max_value, do: 2 23 | def step_value, do: 1 24 | def event_only, do: true 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/console_display.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.ConsoleDisplay do 2 | @moduledoc false 3 | # A simple console based implementation of the HAP.Display behaviour 4 | 5 | @behaviour HAP.Display 6 | 7 | @impl HAP.Display 8 | def display_pairing_code(name, pairing_code, pairing_url) do 9 | IO.puts("\e[1m") 10 | IO.puts("#{name} available for pairing. Connect using the following QR Code") 11 | 12 | pairing_url 13 | |> EQRCode.encode() 14 | |> EQRCode.render() 15 | 16 | IO.puts(""" 17 | \e[1m 18 | Manual Setup Code 19 | ┌────────────┐ 20 | │ #{pairing_code} │ 21 | └────────────┘ 22 | \e[0m 23 | """) 24 | end 25 | 26 | @impl HAP.Display 27 | def clear_pairing_code, do: :ok 28 | 29 | @impl HAP.Display 30 | def identify(name), do: IO.puts("Identifying #{name}") 31 | end 32 | -------------------------------------------------------------------------------- /lib/hap/services/stateless_programmable_switch.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.StatelessProgrammableSwitch do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.stateless-programmable-switch` service 4 | 5 | NOTE special requirements in specification for using Service Label (index) under various circumstances 6 | """ 7 | 8 | defstruct input_event: nil, name: nil, service_label_index: nil 9 | 10 | defimpl HAP.ServiceSource do 11 | def compile(value) do 12 | HAP.Service.ensure_required!(__MODULE__, "input_event", value.input_event) 13 | 14 | %HAP.Service{ 15 | type: "89", 16 | characteristics: [ 17 | {HAP.Characteristics.InputEvent, value.input_event}, 18 | {HAP.Characteristics.Name, value.name}, 19 | {HAP.Characteristics.ServiceLabelIndex, value.service_label_index} 20 | ] 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/hap/http_server.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.HTTPServer do 2 | @moduledoc false 3 | # Defines the HTTP interface for a HomeKit Accessory 4 | 5 | use Plug.Router 6 | 7 | plug(:match) 8 | plug(Plug.Parsers, parsers: [HAP.TLVParser, :json], json_decoder: Jason) 9 | plug(:tidy_headers) 10 | plug(:dispatch) 11 | 12 | def init(opts) do 13 | opts 14 | end 15 | 16 | post("/pair-setup", to: HAP.CleartextHTTPServer) 17 | post("/identify", to: HAP.CleartextHTTPServer) 18 | post("/pair-verify", to: HAP.CleartextHTTPServer) 19 | 20 | post("/pairings", to: HAP.EncryptedHTTPServer) 21 | get("/accessories", to: HAP.EncryptedHTTPServer) 22 | get("/characteristics", to: HAP.EncryptedHTTPServer) 23 | put("/characteristics", to: HAP.EncryptedHTTPServer) 24 | put("/prepare", to: HAP.EncryptedHTTPServer) 25 | 26 | defp tidy_headers(conn, _opts) do 27 | delete_resp_header(conn, "cache-control") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hap/services/light_bulb.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.LightBulb do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.lightbulb` service 4 | """ 5 | 6 | defstruct on: nil, brightness: nil, hue: nil, name: nil, saturation: nil, color_temperature: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "on", value.on) 11 | 12 | %HAP.Service{ 13 | type: "43", 14 | characteristics: [ 15 | {HAP.Characteristics.On, value.on}, 16 | {HAP.Characteristics.Brightness, value.brightness}, 17 | {HAP.Characteristics.Hue, value.hue}, 18 | {HAP.Characteristics.Name, value.name}, 19 | {HAP.Characteristics.Saturation, value.saturation}, 20 | {HAP.Characteristics.ColorTemperature, value.color_temperature} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/leak_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.LeakSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.leak` service 4 | """ 5 | 6 | defstruct leak: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "leak", value.leak) 11 | 12 | %HAP.Service{ 13 | type: "83", 14 | characteristics: [ 15 | {HAP.Characteristics.LeakDetected, value.leak}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/contact_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.ContactSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.contact` service 4 | """ 5 | 6 | defstruct state: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "state", value.state) 11 | 12 | %HAP.Service{ 13 | type: "80", 14 | characteristics: [ 15 | {HAP.Characteristics.ContactSensorState, value.state}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/motion_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.MotionSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.motion` service 4 | """ 5 | 6 | defstruct motion: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "motion", value.motion) 11 | 12 | %HAP.Service{ 13 | type: "85", 14 | characteristics: [ 15 | {HAP.Characteristics.MotionDetected, value.motion}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/occupancy_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.OccupancySensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.occupancy` service 4 | """ 5 | 6 | defstruct occupancy: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "occupancy", value.occupancy) 11 | 12 | %HAP.Service{ 13 | type: "86", 14 | characteristics: [ 15 | {HAP.Characteristics.OccupancyDetected, value.occupancy}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/light_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.LightSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.light` service 4 | """ 5 | 6 | defstruct light_level: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "light_level", value.light_level) 11 | 12 | %HAP.Service{ 13 | type: "84", 14 | characteristics: [ 15 | {HAP.Characteristics.CurrentAmbientLightLevel, value.light_level}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/services/smoke_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.SmokeSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.smoke` service 4 | """ 5 | 6 | defstruct smoke_detected: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "smoke_detected", value.smoke_detected) 11 | 12 | %HAP.Service{ 13 | type: "87", 14 | characteristics: [ 15 | {HAP.Characteristics.SmokeDetected, value.smoke_detected}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/crypto/ecdh.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.ECDH do 2 | @moduledoc false 3 | # Functions to work with Elliptic Curve Diffie-Hellman shared secret generation 4 | 5 | @type public_key :: binary() 6 | @type private_key :: binary() 7 | @type shared_secret :: binary() 8 | 9 | @doc """ 10 | Generates a new ECDH key pair using the `x25519` curve. 11 | 12 | Returns `{:ok, public_key, provate_key}` 13 | """ 14 | @spec key_gen() :: {:ok, public_key(), private_key()} 15 | def key_gen do 16 | {pub, priv} = :crypto.generate_key(:ecdh, :x25519) 17 | {:ok, pub, priv} 18 | end 19 | 20 | @doc """ 21 | Computes a shared secret from the counterpary's public key and our private key, using the `x25519` curve. 22 | 23 | Returns `{:ok, shared secret}` 24 | """ 25 | @spec compute_key(public_key(), private_key()) :: {:ok, shared_secret()} 26 | def compute_key(other_pub, my_priv) do 27 | {:ok, :crypto.compute_key(:ecdh, other_pub, my_priv, :x25519)} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hap/services/temperature_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.TemperatureSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.temperature` service 4 | """ 5 | 6 | defstruct temperature: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "temperature", value.temperature) 11 | 12 | %HAP.Service{ 13 | type: "8A", 14 | characteristics: [ 15 | {HAP.Characteristics.CurrentTemperature, value.temperature}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/test_value_store.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Test.TestValueStore do 2 | @moduledoc """ 3 | A simple GenServer backed value store for testing 4 | """ 5 | 6 | @behaviour HAP.ValueStore 7 | 8 | use GenServer 9 | 10 | def start_link(config) do 11 | GenServer.start_link(__MODULE__, config, name: __MODULE__) 12 | end 13 | 14 | @impl HAP.ValueStore 15 | def get_value(opts) do 16 | GenServer.call(__MODULE__, {:get, opts}) 17 | end 18 | 19 | @impl HAP.ValueStore 20 | def put_value(value, opts) do 21 | GenServer.call(__MODULE__, {:put, value, opts}) 22 | end 23 | 24 | @impl GenServer 25 | def init(_) do 26 | {:ok, %{}} 27 | end 28 | 29 | @impl GenServer 30 | def handle_call({:get, opts}, _from, state) do 31 | {:reply, {:ok, Map.get(state, Keyword.get(opts, :value_name), 0)}, state} 32 | end 33 | 34 | @impl GenServer 35 | def handle_call({:put, value, opts}, _from, state) do 36 | {:reply, :ok, Map.put(state, Keyword.get(opts, :value_name), value)} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/hap/services/humidity_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.HumiditySensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.humidity` service 4 | """ 5 | 6 | defstruct current_relative_humidity: nil, name: nil, active: nil, fault: nil, tampered: nil, low_battery: nil 7 | 8 | defimpl HAP.ServiceSource do 9 | def compile(value) do 10 | HAP.Service.ensure_required!(__MODULE__, "current_relative_humidity", value.current_relative_humidity) 11 | 12 | %HAP.Service{ 13 | type: "82", 14 | characteristics: [ 15 | {HAP.Characteristics.CurrentRelativeHumidity, value.current_relative_humidity}, 16 | {HAP.Characteristics.Name, value.name}, 17 | {HAP.Characteristics.StatusActive, value.active}, 18 | {HAP.Characteristics.StatusFault, value.fault}, 19 | {HAP.Characteristics.StatusTampered, value.tampered}, 20 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 21 | ] 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hap/crypto/hkdf.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.HKDF do 2 | @moduledoc false 3 | # Functions to help with key derivation 4 | 5 | @type ikm :: binary() 6 | @type salt :: binary() 7 | @type info :: binary() 8 | @type session_key :: binary() 9 | 10 | @doc """ 11 | Generates a session key from provided parameters. Uses the SHA-512 as HMAC 12 | 13 | Returns `{:ok, session_key}` 14 | """ 15 | @spec generate(ikm(), salt(), info()) :: {:ok, session_key()} 16 | def generate(ikm, salt, info) do 17 | # Taken from https://github.com/jschneider1207/hkdf/pull/3. Review if/when the 18 | # referenced PR lands 19 | prk = :crypto.mac(:hmac, :sha512, salt, ikm) 20 | 21 | hash_len = :crypto.hash(:sha512, "") |> byte_size() 22 | n = Float.ceil(32 / hash_len) |> round() 23 | 24 | full = 25 | Enum.scan(1..n, "", fn index, prev -> 26 | data = prev <> info <> <> 27 | :crypto.mac(:hmac, :sha512, prk, data) 28 | end) 29 | |> Enum.reduce("", &Kernel.<>(&2, &1)) 30 | 31 | <> = full 32 | {:ok, <>} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mat Trudel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/hap/event_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.EventManager do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | def start_link(arg) do 7 | GenServer.start_link(__MODULE__, arg, name: __MODULE__) 8 | end 9 | 10 | def register(sender, aid, iid) do 11 | GenServer.call(__MODULE__, {:register, sender, aid, iid}) 12 | end 13 | 14 | def unregister(sender, aid, iid) do 15 | GenServer.call(__MODULE__, {:unregister, sender, aid, iid}) 16 | end 17 | 18 | def get_listeners(aid, iid) do 19 | GenServer.call(__MODULE__, {:get_listeners, aid, iid}) 20 | end 21 | 22 | def init(_arg) do 23 | {:ok, %{}} 24 | end 25 | 26 | def handle_call({:register, sender, aid, iid}, _from, state) do 27 | {:reply, {:ok, {aid, iid}}, state |> Map.update({aid, iid}, [sender], fn existing -> [sender | existing] end)} 28 | end 29 | 30 | def handle_call({:unregister, sender, aid, iid}, _from, state) do 31 | {:reply, :ok, state |> Map.update({aid, iid}, [], fn existing -> existing |> List.delete(sender) end)} 32 | end 33 | 34 | def handle_call({:get_listeners, aid, iid}, _from, state) do 35 | {:reply, state |> Map.get({aid, iid}, []), state} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hap/services/slat.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Slat do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.vertical-slat` service 4 | """ 5 | 6 | defstruct current_slat_state: nil, 7 | slat_type: nil, 8 | name: nil, 9 | swing_mode: nil, 10 | current_tilt_angle: nil, 11 | target_tilt_angle: nil 12 | 13 | defimpl HAP.ServiceSource do 14 | def compile(value) do 15 | HAP.Service.ensure_required!(__MODULE__, "current_slat_state", value.current_slat_state) 16 | HAP.Service.ensure_required!(__MODULE__, "slat_type", value.slat_type) 17 | 18 | %HAP.Service{ 19 | type: "B9", 20 | characteristics: [ 21 | {HAP.Characteristics.CurrentSlatState, value.current_slat_state}, 22 | {HAP.Characteristics.SlatType, value.slat_type}, 23 | {HAP.Characteristics.Name, value.name}, 24 | {HAP.Characteristics.SwingMode, value.swing_mode}, 25 | {HAP.Characteristics.CurrentTiltAngle, value.current_tilt_angle}, 26 | {HAP.Characteristics.TargetTiltAngle, value.target_tilt_angle} 27 | ] 28 | } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/hap/services/fan_v2.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.FanV2 do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.fanv2` service 4 | """ 5 | 6 | defstruct active: nil, 7 | name: nil, 8 | current_fan_state: nil, 9 | rotation_direction: nil, 10 | rotation_speed: nil, 11 | swing_mode: nil, 12 | lock_physical_controls: nil 13 | 14 | defimpl HAP.ServiceSource do 15 | def compile(value) do 16 | HAP.Service.ensure_required!(__MODULE__, "active", value.active) 17 | 18 | %HAP.Service{ 19 | type: "B7", 20 | characteristics: [ 21 | {HAP.Characteristics.Active, value.active}, 22 | {HAP.Characteristics.Name, value.name}, 23 | {HAP.Characteristics.CurrentFanState, value.current_fan_state}, 24 | {HAP.Characteristics.RotationDirection, value.rotation_direction}, 25 | {HAP.Characteristics.RotationSpeed, value.rotation_speed}, 26 | {HAP.Characteristics.SwingMode, value.swing_mode}, 27 | {HAP.Characteristics.LockPhysicalControls, value.lock_physical_controls} 28 | ] 29 | } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/hap/identify_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.IdentifyTest do 2 | use ExUnit.Case, async: false 3 | 4 | import ExUnit.CaptureLog 5 | 6 | setup do 7 | {:ok, _pid} = HAP.Test.TestAccessoryServer.test_server() |> start_supervised() 8 | 9 | port = HAP.AccessoryServerManager.port() 10 | {:ok, client} = HAP.Test.HTTPClient.init(:localhost, port) 11 | 12 | {:ok, %{client: client}} 13 | end 14 | 15 | describe "POST /identify" do 16 | test "it should identify itself", context do 17 | assert capture_log(fn -> 18 | {:ok, 204, _headers, _body} = HAP.Test.HTTPClient.post(context.client, "/identify", "") 19 | end) =~ "IDENTIFY Generic HAP Device" 20 | end 21 | 22 | test "It should not succeed if the accessory is paired", context do 23 | # Create the pairing as if it already existed 24 | new_ios_identifier = "BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF" 25 | {:ok, new_ios_ltpk, _new_ios_ltsk} = HAP.Crypto.EDDSA.key_gen() 26 | HAP.AccessoryServerManager.add_controller_pairing(new_ios_identifier, new_ios_ltpk, <<1>>) 27 | 28 | {:ok, 400, _headers, _body} = HAP.Test.HTTPClient.post(context.client, "/identify", "") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/hap/kino_display.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.KinoDisplay do 2 | @moduledoc false 3 | # A Kino based implementation of the HAP.Display behaviour for livebook usage 4 | 5 | @behaviour HAP.Display 6 | 7 | @impl HAP.Display 8 | 9 | if Code.ensure_loaded?(Kino) do 10 | def display_pairing_code(name, pairing_code, pairing_url) do 11 | intro = Kino.Markdown.new("### #{name} available for pairing. Connect using the following QR Code") 12 | 13 | png_qr_code = 14 | pairing_url 15 | |> EQRCode.encode() 16 | |> EQRCode.png() 17 | |> Kino.Image.new("image/png") 18 | 19 | manual_pairing_info = 20 | Kino.Markdown.new(""" 21 | | Manual Setup Code | 22 | | -- | 23 | | #{pairing_code} | 24 | """) 25 | 26 | Kino.render(intro) 27 | Kino.render(png_qr_code) 28 | Kino.render(manual_pairing_info) 29 | end 30 | else 31 | def display_pairing_code(_name, _pairing_code, _pairing_url) do 32 | IO.puts("Kino not available - use other HAP display module, or ensure Kino is installed") 33 | end 34 | end 35 | 36 | @impl HAP.Display 37 | def clear_pairing_code, do: :ok 38 | 39 | @impl HAP.Display 40 | def identify(name), do: IO.puts("Identifying #{name}") 41 | end 42 | -------------------------------------------------------------------------------- /lib/hap/services/door.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Door do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.door` service 4 | """ 5 | 6 | defstruct current_position: nil, 7 | target_position: nil, 8 | position_state: nil, 9 | name: nil, 10 | hold_position: nil, 11 | obstruction_detected: nil 12 | 13 | defimpl HAP.ServiceSource do 14 | def compile(value) do 15 | HAP.Service.ensure_required!(__MODULE__, "current_position", value.current_position) 16 | HAP.Service.ensure_required!(__MODULE__, "target_position", value.target_position) 17 | HAP.Service.ensure_required!(__MODULE__, "position_state", value.position_state) 18 | 19 | %HAP.Service{ 20 | type: "81", 21 | characteristics: [ 22 | {HAP.Characteristics.CurrentPosition, value.current_position}, 23 | {HAP.Characteristics.TargetPosition, value.target_position}, 24 | {HAP.Characteristics.PositionState, value.position_state}, 25 | {HAP.Characteristics.Name, value.name}, 26 | {HAP.Characteristics.HoldPosition, value.hold_position}, 27 | {HAP.Characteristics.ObstructionDetected, value.obstruction_detected} 28 | ] 29 | } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hap/services/window.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Window do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.window` service 4 | """ 5 | 6 | defstruct current_position: nil, 7 | target_position: nil, 8 | position_state: nil, 9 | name: nil, 10 | hold_position: nil, 11 | obstruction_detected: nil 12 | 13 | defimpl HAP.ServiceSource do 14 | def compile(value) do 15 | HAP.Service.ensure_required!(__MODULE__, "current_position", value.current_position) 16 | HAP.Service.ensure_required!(__MODULE__, "target_position", value.target_position) 17 | HAP.Service.ensure_required!(__MODULE__, "position_state", value.position_state) 18 | 19 | %HAP.Service{ 20 | type: "8B", 21 | characteristics: [ 22 | {HAP.Characteristics.CurrentPosition, value.current_position}, 23 | {HAP.Characteristics.TargetPosition, value.target_position}, 24 | {HAP.Characteristics.PositionState, value.position_state}, 25 | {HAP.Characteristics.Name, value.name}, 26 | {HAP.Characteristics.HoldPosition, value.hold_position}, 27 | {HAP.Characteristics.ObstructionDetected, value.obstruction_detected} 28 | ] 29 | } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hap/crypto/eddsa.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.EDDSA do 2 | @moduledoc false 3 | # Functions to generate keys, sign & verify messages using Elliptic Curve Signatures 4 | 5 | @type plaintext :: binary() 6 | @type public_key :: binary() 7 | @type private_key :: binary() 8 | @type signature :: binary() 9 | 10 | @doc """ 11 | Generates a new signing key pair using the `ed25519` signature scheme. 12 | 13 | Returns `{:ok, public_key, private_key}` 14 | """ 15 | @spec key_gen() :: {:ok, public_key(), private_key()} 16 | def key_gen do 17 | {pub, priv} = :crypto.generate_key(:eddsa, :ed25519) 18 | {:ok, pub, priv} 19 | end 20 | 21 | @doc """ 22 | Signs the given message with the given `ed25519` private key. 23 | 24 | Returns `{:ok, signature}` 25 | """ 26 | @spec sign(plaintext(), private_key()) :: {:ok, signature()} 27 | def sign(message, key) do 28 | {:ok, :crypto.sign(:eddsa, :sha512, message, [key, :ed25519])} 29 | end 30 | 31 | @doc """ 32 | Verifies that the given signature signs the given message under the key specified. 33 | 34 | Returns `{:ok, true}` or `{:ok, false}` 35 | """ 36 | @spec verify(plaintext(), signature(), public_key()) :: {:ok, boolean()} 37 | def verify(message, signature, key) do 38 | {:ok, :crypto.verify(:eddsa, :sha512, message, signature, [key, :ed25519])} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/hap/services/television_speaker.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.TelevisionSpeaker do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.television-speaker` service 4 | 5 | The TelevisionSpeaker service represents the audio output capabilities of a Television. 6 | It is typically linked to a Television service and provides volume control and mute functionality. 7 | """ 8 | 9 | defstruct mute: nil, 10 | active: nil, 11 | volume_control_type: nil, 12 | volume_selector: nil, 13 | name: nil, 14 | volume: nil 15 | 16 | defimpl HAP.ServiceSource do 17 | def compile(value) do 18 | HAP.Service.ensure_required!(__MODULE__, "mute", value.mute) 19 | HAP.Service.ensure_required!(__MODULE__, "volume_control_type", value.volume_control_type) 20 | 21 | %HAP.Service{ 22 | type: "113", 23 | characteristics: [ 24 | {HAP.Characteristics.Mute, value.mute}, 25 | {HAP.Characteristics.Active, value.active}, 26 | {HAP.Characteristics.VolumeControlType, value.volume_control_type}, 27 | {HAP.Characteristics.VolumeSelector, value.volume_selector}, 28 | {HAP.Characteristics.Name, value.name}, 29 | {HAP.Characteristics.Volume, value.volume} 30 | ] 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/hap/services/accessory_information.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.AccessoryInformation do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.accessory-information` service 4 | """ 5 | 6 | @behaviour HAP.ValueStore 7 | 8 | defstruct accessory: nil 9 | 10 | defimpl HAP.ServiceSource do 11 | def compile(%HAP.Services.AccessoryInformation{accessory: %HAP.Accessory{} = accessory}) do 12 | %HAP.Service{ 13 | type: "3E", 14 | characteristics: [ 15 | {HAP.Characteristics.Name, accessory.name}, 16 | {HAP.Characteristics.Model, accessory.model}, 17 | {HAP.Characteristics.Manufacturer, accessory.manufacturer}, 18 | {HAP.Characteristics.SerialNumber, accessory.serial_number}, 19 | {HAP.Characteristics.FirmwareRevision, accessory.firmware_revision}, 20 | {HAP.Characteristics.Identify, {HAP.Services.AccessoryInformation, name: accessory.name}} 21 | ] 22 | } 23 | end 24 | end 25 | 26 | @impl HAP.ValueStore 27 | def get_value(_) do 28 | raise "Cannot get value for identify" 29 | end 30 | 31 | @impl HAP.ValueStore 32 | def put_value(_value, name: name) do 33 | # identify is a GenServer call that otherwise would be calling its own process 34 | Task.start(fn -> HAP.Display.identify(name) end) 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hap/services/garage_door.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.GarageDoor do 2 | @moduledoc """ 3 | Struct representing an instance of 'public.hap.service.garage_door.opener' service 4 | """ 5 | 6 | defstruct current_door_state: nil, 7 | target_door_state: nil, 8 | name: nil, 9 | lock_current_state: nil, 10 | lock_target_state: nil, 11 | obstruction_detected: nil 12 | 13 | defimpl HAP.ServiceSource do 14 | def compile(value) do 15 | HAP.Service.ensure_required!(__MODULE__, "current_door_state", value.current_door_state) 16 | HAP.Service.ensure_required!(__MODULE__, "target_door_state", value.target_door_state) 17 | HAP.Service.ensure_required!(__MODULE__, "obstruction_detected", value.obstruction_detected) 18 | 19 | %HAP.Service{ 20 | type: "41", 21 | characteristics: [ 22 | {HAP.Characteristics.CurrentDoorState, value.current_door_state}, 23 | {HAP.Characteristics.TargetDoorState, value.target_door_state}, 24 | {HAP.Characteristics.ObstructionDetected, value.obstruction_detected}, 25 | {HAP.Characteristics.Name, value.name}, 26 | {HAP.Characteristics.LockCurrentState, value.lock_current_state}, 27 | {HAP.Characteristics.LockTargetState, value.lock_target_state} 28 | ] 29 | } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hap/services/carbon_dioxide_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.CarbonDioxideSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.carbon-dioxide` service 4 | """ 5 | 6 | defstruct carbon_dioxide_detected: nil, 7 | carbon_dioxide_level: nil, 8 | carbon_dioxide_peak_level: nil, 9 | name: nil, 10 | active: nil, 11 | fault: nil, 12 | tampered: nil, 13 | low_battery: nil 14 | 15 | defimpl HAP.ServiceSource do 16 | def compile(value) do 17 | HAP.Service.ensure_required!( 18 | __MODULE__, 19 | "carbon_dioxide_detected", 20 | value.carbon_dioxide_detected 21 | ) 22 | 23 | %HAP.Service{ 24 | type: "97", 25 | characteristics: [ 26 | {HAP.Characteristics.CarbonDioxideDetected, value.carbon_dioxide_detected}, 27 | {HAP.Characteristics.CarbonDioxideLevel, value.carbon_dioxide_level}, 28 | {HAP.Characteristics.CarbonDioxidePeakLevel, value.carbon_dioxide_peak_level}, 29 | {HAP.Characteristics.Name, value.name}, 30 | {HAP.Characteristics.StatusActive, value.active}, 31 | {HAP.Characteristics.StatusFault, value.fault}, 32 | {HAP.Characteristics.StatusTampered, value.tampered}, 33 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 34 | ] 35 | } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/hap/services/carbon_monoxide_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.CarbonMonoxideSensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.carbon-monoxide` service 4 | """ 5 | 6 | defstruct carbon_monoxide_detected: nil, 7 | carbon_monoxide_level: nil, 8 | carbon_monoxide_peak_level: nil, 9 | name: nil, 10 | active: nil, 11 | fault: nil, 12 | tampered: nil, 13 | low_battery: nil 14 | 15 | defimpl HAP.ServiceSource do 16 | def compile(value) do 17 | HAP.Service.ensure_required!( 18 | __MODULE__, 19 | "carbon_monoxide_detected", 20 | value.carbon_monoxide_detected 21 | ) 22 | 23 | %HAP.Service{ 24 | type: "7F", 25 | characteristics: [ 26 | {HAP.Characteristics.CarbonMonoxideDetected, value.carbon_monoxide_detected}, 27 | {HAP.Characteristics.CarbonMonoxideLevel, value.carbon_monoxide_level}, 28 | {HAP.Characteristics.CarbonMonoxidePeakLevel, value.carbon_monoxide_peak_level}, 29 | {HAP.Characteristics.Name, value.name}, 30 | {HAP.Characteristics.StatusActive, value.active}, 31 | {HAP.Characteristics.StatusFault, value.fault}, 32 | {HAP.Characteristics.StatusTampered, value.tampered}, 33 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 34 | ] 35 | } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/hap/discovery.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Discovery do 2 | @moduledoc false 3 | # Provides functions to define & update a `HAP.Accessory` advertisement via multicast DNS according to Section 6 of 4 | # Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 5 | 6 | require Logger 7 | 8 | @doc false 9 | def reload do 10 | Logger.debug("(Re-)Advertising mDNS record") 11 | 12 | <> = 13 | :crypto.hash(:sha512, HAP.AccessoryServerManager.setup_id() <> HAP.AccessoryServerManager.identifier()) 14 | 15 | identifier_atom = HAP.AccessoryServerManager.identifier() |> String.to_atom() 16 | 17 | MdnsLite.remove_mdns_service(identifier_atom) 18 | 19 | %{ 20 | id: identifier_atom, 21 | instance_name: HAP.AccessoryServerManager.name(), 22 | protocol: "hap", 23 | transport: "tcp", 24 | port: HAP.AccessoryServerManager.port(), 25 | txt_payload: [ 26 | "c#=#{HAP.AccessoryServerManager.config_number()}", 27 | "ff=0", 28 | "id=#{HAP.AccessoryServerManager.identifier()}", 29 | "md=#{HAP.AccessoryServerManager.model()}", 30 | "pv=1.1", 31 | "s#=1", 32 | "sf=#{if HAP.AccessoryServerManager.paired?(), do: 0, else: 1}", 33 | "ci=#{HAP.AccessoryServerManager.accessory_type()}", 34 | "sh=#{setup_hash |> Base.encode64()}" 35 | ] 36 | } 37 | |> MdnsLite.add_mdns_service() 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/hap/services/air_purifier.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.AirPurifier do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.air-purifier` service 4 | """ 5 | 6 | defstruct active: nil, 7 | current_air_purifier_state: nil, 8 | target_air_purifier_state: nil, 9 | name: nil, 10 | rotation_speed: nil, 11 | swing_mode: nil, 12 | lock_physical_controls: nil 13 | 14 | defimpl HAP.ServiceSource do 15 | def compile(value) do 16 | HAP.Service.ensure_required!(__MODULE__, "active", value.active) 17 | HAP.Service.ensure_required!(__MODULE__, "current_air_purifier_state", value.current_air_purifier_state) 18 | HAP.Service.ensure_required!(__MODULE__, "target_air_purifier_state", value.target_air_purifier_state) 19 | 20 | %HAP.Service{ 21 | type: "BB", 22 | characteristics: [ 23 | {HAP.Characteristics.Active, value.active}, 24 | {HAP.Characteristics.CurrentAirPurifierState, value.current_air_purifier_state}, 25 | {HAP.Characteristics.TargetAirPurifierState, value.target_air_purifier_state}, 26 | {HAP.Characteristics.Name, value.name}, 27 | {HAP.Characteristics.RotationSpeed, value.rotation_speed}, 28 | {HAP.Characteristics.SwingMode, value.swing_mode}, 29 | {HAP.Characteristics.LockPhysicalControls, value.lock_physical_controls} 30 | ] 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/hap/hap_session_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.HAPSessionHandler do 2 | @moduledoc false 3 | # A thin wrapper around Bandit's HTTP1 support, which does two things: 4 | # 5 | # 1. HAP requires socket-level encryption at least part of the time. This handler implementation 6 | # shims such support into the `c:handle_data/3` callback 7 | # 2. Provides for the ability to send async (and non-standard) `EVENT` messages to a client 8 | 9 | use ThousandIsland.Handler 10 | 11 | # Push an asynchronous message to the client as described in section 6.8 of the 12 | # HomeKit Accessory Protocol specification 13 | def push(pid, data) do 14 | GenServer.cast(pid, {:push, data}) 15 | end 16 | 17 | @impl ThousandIsland.Handler 18 | def handle_data(data, socket, state) do 19 | {:ok, data} = HAP.HAPSessionTransport.decrypt_if_needed(data) 20 | Bandit.HTTP1.Handler.handle_data(data, socket, state) 21 | end 22 | 23 | @impl GenServer 24 | def handle_cast({:push, data}, {socket, state}) do 25 | data = Jason.encode!(data) 26 | 27 | headers = %{ 28 | "content-length" => data |> byte_size() |> to_string(), 29 | "content-type" => "application/hap+json" 30 | } 31 | 32 | to_send = [ 33 | "EVENT/1.0 200 OK\r\n", 34 | Enum.map(headers, fn {k, v} -> [k, ": ", v, "\r\n"] end), 35 | "\r\n", 36 | data 37 | ] 38 | 39 | ThousandIsland.Socket.send(socket, to_send) 40 | 41 | {:noreply, {socket, state}} 42 | end 43 | 44 | def handle_info(msg, state), do: Bandit.HTTP1.Handler.handle_info(msg, state) 45 | end 46 | -------------------------------------------------------------------------------- /lib/hap/tlv_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.TLVParser do 2 | @moduledoc false 3 | # A `Plug.Parsers` compliant parser for TLV payloads as described in the Appendix of 4 | # Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 5 | 6 | @behaviour Plug.Parsers 7 | 8 | @impl Plug.Parsers 9 | def init(opts), do: opts 10 | 11 | @impl Plug.Parsers 12 | def parse(conn, "application", "pairing+tlv8", _params, _opts) do 13 | {:ok, body, conn} = Plug.Conn.read_body(conn) 14 | {:ok, parse_tlv(body), conn} 15 | end 16 | 17 | def parse(conn, _type, _subtype, _params, _opts), do: {:next, conn} 18 | 19 | @doc """ 20 | Parses a TLV binary into a map (does not allow for repeating keys) 21 | """ 22 | def parse_tlv(str) do 23 | str 24 | |> Stream.unfold(&next_tag/1) 25 | |> Map.new() 26 | end 27 | 28 | @doc """ 29 | Parses a TLV binary into a keyword list (allows for repeating keys) 30 | """ 31 | def parse_tlv_as_keyword(str) do 32 | str 33 | |> Stream.unfold(&next_tag/1) 34 | |> Enum.map(fn {k, v} -> {k |> Integer.to_string() |> String.to_atom(), v} end) 35 | end 36 | 37 | defp next_tag(str) do 38 | case str do 39 | <> when tag == next_tag -> 40 | {{_, next_value}, next_rest} = next_tag(<> <> rest) 41 | {{tag, value <> next_value}, next_rest} 42 | 43 | <> -> 44 | <> = rest 45 | {{tag, value}, rest} 46 | 47 | <<>> -> 48 | nil 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/hap/services/air_quality_sensor.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.AirQualitySensor do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.sensor.air-quality` service 4 | """ 5 | 6 | defstruct air_quality: nil, 7 | name: nil, 8 | ozone_density: nil, 9 | nitrogen_dioxide_density: nil, 10 | sulphur_dioxide_density: nil, 11 | pm2_5_density: nil, 12 | pm10_density: nil, 13 | voc_density: nil, 14 | active: nil, 15 | fault: nil, 16 | tampered: nil, 17 | low_battery: nil 18 | 19 | defimpl HAP.ServiceSource do 20 | def compile(value) do 21 | HAP.Service.ensure_required!(__MODULE__, "air_quality", value.air_quality) 22 | 23 | %HAP.Service{ 24 | type: "8D", 25 | characteristics: [ 26 | {HAP.Characteristics.AirQuality, value.air_quality}, 27 | {HAP.Characteristics.Name, value.name}, 28 | {Hap.Characteristics.OzoneDensity, value.ozone_density}, 29 | {HAP.Characteristics.NitrogenDioxideDensity, value.nitrogen_dioxide_density}, 30 | {HAP.Characteristics.SulphurDioxideDensity, value.sulphur_dioxide_density}, 31 | {HAP.Characteristics.PM25Density, value.pm2_5_density}, 32 | {HAP.Characteristics.PM10Density, value.pm10_density}, 33 | {HAP.Characteristics.VOCDensity, value.voc_density}, 34 | {HAP.Characteristics.StatusActive, value.active}, 35 | {HAP.Characteristics.StatusFault, value.fault}, 36 | {HAP.Characteristics.StatusTampered, value.tampered}, 37 | {HAP.Characteristics.StatusLowBattery, value.low_battery} 38 | ] 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/hap/service.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Service do 2 | @moduledoc """ 3 | Represents a single service, containing a number of characteristics 4 | """ 5 | 6 | defstruct type: nil, characteristics: [] 7 | 8 | @typedoc """ 9 | Represents a service of a given type, containing a number of characteristics 10 | """ 11 | @type t :: %__MODULE__{ 12 | type: type(), 13 | characteristics: [HAP.Characteristic.t()] 14 | } 15 | 16 | @typedoc """ 17 | The type of a service as defined in Section 6.6.1 of Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 18 | """ 19 | @type type :: String.t() 20 | 21 | @doc false 22 | @spec compile(HAP.ServiceSource.t()) :: t() 23 | def compile(source) do 24 | service = source |> HAP.ServiceSource.compile() 25 | 26 | characteristics = service.characteristics |> Enum.reject(fn {_characteristic_mod, value} -> is_nil(value) end) 27 | 28 | %{service | characteristics: characteristics} 29 | end 30 | 31 | @doc false 32 | def ensure_required!(module, name, nil), do: raise("Value for #{name} required for service definition #{module}") 33 | def ensure_required!(_module, _name, _characteristic_value), do: :ok 34 | 35 | @doc false 36 | def get_characteristic(%__MODULE__{characteristics: characteristics}, iid) do 37 | with {:ok, characteristic_index} <- HAP.IID.characteristic_index(iid), 38 | characteristic when not is_nil(characteristic) <- Enum.at(characteristics, characteristic_index) do 39 | {:ok, characteristic} 40 | else 41 | _ -> {:error, -70_409} 42 | end 43 | end 44 | 45 | # Provide an identity transform for services to allow for direct definition of services within a `HAP.Accessory` 46 | defimpl HAP.ServiceSource do 47 | def compile(value), do: value 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/hap/persistent_storage.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.PersistentStorage do 2 | @moduledoc false 3 | # Encapsulates a simple persistent key-value store 4 | 5 | use GenServer 6 | 7 | def start_link(path) do 8 | GenServer.start_link(__MODULE__, path, name: __MODULE__) 9 | end 10 | 11 | @doc false 12 | def get(key, default \\ nil, pid \\ __MODULE__), do: GenServer.call(pid, {:get, key, default}) 13 | 14 | @doc false 15 | def put(key, value, pid \\ __MODULE__), do: GenServer.call(pid, {:put, key, value}) 16 | 17 | @doc false 18 | def put_new_lazy(key, func, pid \\ __MODULE__), do: GenServer.call(pid, {:put_new_lazy, key, func}) 19 | 20 | @doc false 21 | def get_and_update(key, func, pid \\ __MODULE__), do: GenServer.call(pid, {:get_and_update, key, func}) 22 | 23 | @doc false 24 | def clear(pid \\ __MODULE__), do: GenServer.call(pid, :clear) 25 | 26 | def init(path) do 27 | {:ok, cub_pid} = CubDB.start_link(path) 28 | 29 | {:ok, %{cub_pid: cub_pid}} 30 | end 31 | 32 | def handle_call({:get, key, default}, _from, %{cub_pid: cub_pid} = state) do 33 | {:reply, CubDB.get(cub_pid, key, default), state} 34 | end 35 | 36 | def handle_call({:put, key, value}, _from, %{cub_pid: cub_pid} = state) do 37 | {:reply, CubDB.put(cub_pid, key, value), state} 38 | end 39 | 40 | def handle_call({:put_new_lazy, key, func}, _from, %{cub_pid: cub_pid} = state) do 41 | if CubDB.has_key?(cub_pid, key) do 42 | {:reply, :ok, state} 43 | else 44 | {:reply, CubDB.put(cub_pid, key, func.()), state} 45 | end 46 | end 47 | 48 | def handle_call({:get_and_update, key, func}, _from, %{cub_pid: cub_pid} = state) do 49 | {:reply, {:ok, CubDB.get_and_update(cub_pid, key, func)}, state} 50 | end 51 | 52 | def handle_call(:clear, _from, %{cub_pid: cub_pid} = state) do 53 | {:reply, {:ok, CubDB.clear(cub_pid)}, state} 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/hap/services/window_covering.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.WindowCovering do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.window-covering` service 4 | """ 5 | 6 | defstruct current_position: nil, 7 | target_position: nil, 8 | position_state: nil, 9 | name: nil, 10 | hold_position: nil, 11 | current_horizontal_tilt_angle: nil, 12 | target_horizontal_tilt_angle: nil, 13 | current_vertical_tilt_angle: nil, 14 | target_vertical_tilt_angle: nil, 15 | obstruction_detected: nil 16 | 17 | defimpl HAP.ServiceSource do 18 | def compile(value) do 19 | HAP.Service.ensure_required!(__MODULE__, "current_position", value.current_position) 20 | HAP.Service.ensure_required!(__MODULE__, "target_position", value.target_position) 21 | HAP.Service.ensure_required!(__MODULE__, "position_state", value.position_state) 22 | 23 | %HAP.Service{ 24 | type: "8C", 25 | characteristics: [ 26 | {HAP.Characteristics.CurrentPosition, value.current_position}, 27 | {HAP.Characteristics.TargetPosition, value.target_position}, 28 | {HAP.Characteristics.PositionState, value.position_state}, 29 | {HAP.Characteristics.Name, value.name}, 30 | {HAP.Characteristics.HoldPosition, value.hold_position}, 31 | {HAP.Characteristics.CurrentHorizontalTiltAngle, value.current_horizontal_tilt_angle}, 32 | {HAP.Characteristics.TargetHorizontalTiltAngle, value.target_horizontal_tilt_angle}, 33 | {HAP.Characteristics.CurrentVerticalTiltAngle, value.current_vertical_tilt_angle}, 34 | {HAP.Characteristics.TargetVerticalTiltAngle, value.target_vertical_tilt_angle}, 35 | {HAP.Characteristics.ObstructionDetected, value.obstruction_detected} 36 | ] 37 | } 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/hap/services/input_source.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.InputSource do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.input-source` service 4 | 5 | The InputSource service represents an individual input on a Television, 6 | such as HDMI ports, apps, or other input methods. Multiple InputSource 7 | services can be linked to a Television service. 8 | """ 9 | 10 | defstruct configured_name: nil, 11 | input_source_type: nil, 12 | is_configured: nil, 13 | current_visibility_state: nil, 14 | identifier: nil, 15 | input_device_type: nil, 16 | name: nil, 17 | target_visibility_state: nil 18 | 19 | defimpl HAP.ServiceSource do 20 | def compile(value) do 21 | HAP.Service.ensure_required!(__MODULE__, "configured_name", value.configured_name) 22 | HAP.Service.ensure_required!(__MODULE__, "input_source_type", value.input_source_type) 23 | HAP.Service.ensure_required!(__MODULE__, "is_configured", value.is_configured) 24 | HAP.Service.ensure_required!(__MODULE__, "current_visibility_state", value.current_visibility_state) 25 | HAP.Service.ensure_required!(__MODULE__, "identifier", value.identifier) 26 | 27 | %HAP.Service{ 28 | type: "D9", 29 | characteristics: [ 30 | {HAP.Characteristics.ConfiguredName, value.configured_name}, 31 | {HAP.Characteristics.InputSourceType, value.input_source_type}, 32 | {HAP.Characteristics.IsConfigured, value.is_configured}, 33 | {HAP.Characteristics.CurrentVisibilityState, value.current_visibility_state}, 34 | {HAP.Characteristics.Identifier, value.identifier}, 35 | {HAP.Characteristics.InputDeviceType, value.input_device_type}, 36 | {HAP.Characteristics.Name, value.name}, 37 | {HAP.Characteristics.TargetVisibilityState, value.target_visibility_state} 38 | ] 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/hap/services/heater_cooler.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.HeaterCooler do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.heater-cooler` service 4 | """ 5 | 6 | defstruct active: nil, 7 | current_temp: nil, 8 | current_state: nil, 9 | target_state: nil, 10 | name: nil, 11 | rotation_speed: nil, 12 | temp_display_units: nil, 13 | swing_mode: nil, 14 | cooling_threshold_temp: nil, 15 | heating_threshold_temp: nil, 16 | lock_physical_controls: nil 17 | 18 | defimpl HAP.ServiceSource do 19 | def compile(value) do 20 | HAP.Service.ensure_required!(__MODULE__, "active", value.active) 21 | HAP.Service.ensure_required!(__MODULE__, "current_temp", value.current_temp) 22 | HAP.Service.ensure_required!(__MODULE__, "current_state", value.current_state) 23 | HAP.Service.ensure_required!(__MODULE__, "target_state", value.target_state) 24 | 25 | %HAP.Service{ 26 | type: "BC", 27 | characteristics: [ 28 | {HAP.Characteristics.Active, value.active}, 29 | {HAP.Characteristics.CurrentTemperature, value.current_temp}, 30 | {HAP.Characteristics.CurrentHeaterCoolerState, value.current_state}, 31 | {HAP.Characteristics.TargetHeaterCoolerState, value.target_state}, 32 | {HAP.Characteristics.Name, value.name}, 33 | {HAP.Characteristics.RotationSpeed, value.rotation_speed}, 34 | {HAP.Characteristics.TemperatureDisplayUnits, value.temp_display_units}, 35 | {HAP.Characteristics.SwingMode, value.swing_mode}, 36 | {HAP.Characteristics.CoolingThresholdTemperature, value.cooling_threshold_temp}, 37 | {HAP.Characteristics.HeatingThresholdTemperature, value.heating_threshold_temp}, 38 | {HAP.Characteristics.LockPhysicalControls, value.lock_physical_controls} 39 | ] 40 | } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/hap/services/thermostat.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Thermostat do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.thermostat` service 4 | """ 5 | 6 | defstruct cooling_threshold_temp: nil, 7 | current_humidity: nil, 8 | current_state: nil, 9 | current_temp: nil, 10 | heating_threshold_temp: nil, 11 | name: nil, 12 | target_humidity: nil, 13 | target_state: nil, 14 | target_temp: nil, 15 | temp_display_units: nil 16 | 17 | defimpl HAP.ServiceSource do 18 | def compile(value) do 19 | HAP.Service.ensure_required!(__MODULE__, "current_state", value.current_state) 20 | HAP.Service.ensure_required!(__MODULE__, "current_temp", value.current_temp) 21 | HAP.Service.ensure_required!(__MODULE__, "target_state", value.target_state) 22 | HAP.Service.ensure_required!(__MODULE__, "target_temp", value.target_temp) 23 | HAP.Service.ensure_required!(__MODULE__, "temp_display_units", value.temp_display_units) 24 | 25 | %HAP.Service{ 26 | type: "4A", 27 | characteristics: [ 28 | {HAP.Characteristics.CoolingThresholdTemperature, value.cooling_threshold_temp}, 29 | {HAP.Characteristics.CurrentHeatingCoolingState, value.current_state}, 30 | {HAP.Characteristics.CurrentRelativeHumidity, value.current_humidity}, 31 | {HAP.Characteristics.CurrentTemperature, value.current_temp}, 32 | {HAP.Characteristics.HeatingThresholdTemperature, value.heating_threshold_temp}, 33 | {HAP.Characteristics.Name, value.name}, 34 | {HAP.Characteristics.TargetHeaterCoolerState, value.target_state}, 35 | {HAP.Characteristics.TargetRelativeHumidity, value.target_humidity}, 36 | {HAP.Characteristics.TargetTemperature, value.target_temp}, 37 | {HAP.Characteristics.TemperatureDisplayUnits, value.temp_display_units} 38 | ] 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/hap/display.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Display do 2 | @moduledoc """ 3 | A behaviour which encapsulates all user-facing display concerns for an accessory. Applications which use HAP may 4 | provide their own implementation of this behaviour as a field in a `HAP.AccessoryServer`. If no such 5 | implementation is provided HAP uses a default console based implementation 6 | """ 7 | 8 | @doc """ 9 | Display a notification to the user containing information on how to pair with 10 | this accessory server. The value of `pairing_url` can be encoded in a QR code 11 | to enable pairing directly from an iOS device. 12 | """ 13 | @callback display_pairing_code( 14 | name :: HAP.AccessoryServer.name(), 15 | pairing_code :: HAP.AccessoryServer.pairing_code(), 16 | pairing_url :: HAP.AccessoryServer.pairing_url() 17 | ) :: 18 | any() 19 | 20 | @doc """ 21 | Stop displaying any currently displayed pairing information to the user. This is 22 | most commonly because a pairing has been established with a controller 23 | """ 24 | @callback clear_pairing_code() :: any() 25 | 26 | @doc """ 27 | Display a notification to the user that identifies the named device or accessory. 28 | This comes from a user request within the Home app to identify the given device 29 | or accessory. 30 | """ 31 | @callback identify(name :: String.t()) :: any() 32 | 33 | @doc false 34 | def update_pairing_info_display do 35 | display_module = HAP.AccessoryServerManager.display_module() 36 | 37 | if HAP.AccessoryServerManager.paired?() do 38 | display_module.clear_pairing_code() 39 | else 40 | name = HAP.AccessoryServerManager.name() 41 | pairing_code = HAP.AccessoryServerManager.pairing_code() 42 | pairing_url = HAP.AccessoryServerManager.pairing_url() 43 | display_module.display_pairing_code(name, pairing_code, pairing_url) 44 | end 45 | end 46 | 47 | @doc false 48 | def identify(name) do 49 | HAP.AccessoryServerManager.display_module().identify(name) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/hap/tlv_parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.TLVParserTest do 2 | use ExUnit.Case 3 | 4 | import Plug.Test 5 | 6 | test "parses multi-segment entries properly in a conn" do 7 | data = 8 | <<1, 255>> <> 9 | :binary.copy(<<0xAA>>, 255) <> 10 | <<1, 255>> <> :binary.copy(<<0xAB>>, 255) <> <<1, 10>> <> :binary.copy(<<0xBA>>, 10) <> <<2, 1, 3>> 11 | 12 | expected = %{ 13 | 1 => :binary.copy(<<0xAA>>, 255) <> :binary.copy(<<0xAB>>, 255) <> :binary.copy(<<0xBA>>, 10), 14 | 2 => <<3>> 15 | } 16 | 17 | {:ok, result, _conn} = 18 | conn("METHOD", "/path", data) 19 | |> HAP.TLVParser.parse("application", "pairing+tlv8", {}, {}) 20 | 21 | assert result == expected 22 | end 23 | 24 | test "parses multi-segment entries properly as a string" do 25 | data = 26 | <<1, 255>> <> 27 | :binary.copy(<<0xAA>>, 255) <> 28 | <<1, 255>> <> :binary.copy(<<0xAB>>, 255) <> <<1, 10>> <> :binary.copy(<<0xBA>>, 10) <> <<2, 1, 3>> 29 | 30 | expected = %{ 31 | 1 => :binary.copy(<<0xAA>>, 255) <> :binary.copy(<<0xAB>>, 255) <> :binary.copy(<<0xBA>>, 10), 32 | 2 => <<3>> 33 | } 34 | 35 | assert HAP.TLVParser.parse_tlv(data) == expected 36 | end 37 | 38 | test "parses multi-segment entries into a keyword list" do 39 | data = 40 | <<1, 255>> <> 41 | :binary.copy(<<0xAA>>, 255) <> 42 | <<1, 255>> <> 43 | :binary.copy(<<0xAB>>, 255) <> 44 | <<1, 10>> <> 45 | :binary.copy(<<0xBA>>, 10) <> 46 | <<2, 1, 3>> <> 47 | <<1, 255>> <> 48 | :binary.copy(<<0xAA>>, 255) <> 49 | <<1, 255>> <> :binary.copy(<<0xAB>>, 255) <> <<1, 10>> <> :binary.copy(<<0xBA>>, 10) <> <<2, 1, 3>> 50 | 51 | expected = [ 52 | "1": :binary.copy(<<0xAA>>, 255) <> :binary.copy(<<0xAB>>, 255) <> :binary.copy(<<0xBA>>, 10), 53 | "2": <<3>>, 54 | "1": :binary.copy(<<0xAA>>, 255) <> :binary.copy(<<0xAB>>, 255) <> :binary.copy(<<0xBA>>, 10), 55 | "2": <<3>> 56 | ] 57 | 58 | assert HAP.TLVParser.parse_tlv_as_keyword(data) == expected 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/hap/cleartext_http_server.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.CleartextHTTPServer do 2 | @moduledoc false 3 | # Defines the HTTP interface for a HomeKit Accessory which may only be accessed over 4 | # a non-encrpyted channel 5 | 6 | use Plug.Router 7 | 8 | plug(:match) 9 | plug(:prohibit_authenticated_session) 10 | plug(:dispatch) 11 | 12 | def init(opts) do 13 | opts 14 | end 15 | 16 | post "/identify" do 17 | if HAP.AccessoryServerManager.paired?() do 18 | conn 19 | |> send_resp(400, "Already Paired") 20 | else 21 | HAP.AccessoryServerManager.name() |> HAP.Display.identify() 22 | 23 | conn 24 | |> send_resp(204, "No Content") 25 | end 26 | end 27 | 28 | post "/pair-setup" do 29 | conn.body_params 30 | |> HAP.PairSetup.handle_message() 31 | |> case do 32 | {:ok, response} -> 33 | conn 34 | |> put_resp_header("content-type", "application/pairing+tlv8") 35 | |> send_resp(200, HAP.TLVEncoder.to_binary(response)) 36 | 37 | {:error, reason} -> 38 | conn 39 | |> send_resp(400, reason) 40 | end 41 | end 42 | 43 | post "/pair-verify" do 44 | conn.body_params 45 | |> HAP.PairVerify.handle_message(HAP.HAPSessionTransport.get_pair_state()) 46 | |> case do 47 | {:ok, response, new_state, accessory_to_controller_key, controller_to_accessory_key} -> 48 | conn = 49 | conn 50 | |> put_resp_header("content-type", "application/pairing+tlv8") 51 | |> send_resp(200, HAP.TLVEncoder.to_binary(response)) 52 | 53 | HAP.HAPSessionTransport.put_pair_state(new_state) 54 | HAP.HAPSessionTransport.put_send_key(accessory_to_controller_key) 55 | HAP.HAPSessionTransport.put_recv_key(controller_to_accessory_key) 56 | 57 | conn 58 | 59 | {:error, reason} -> 60 | conn 61 | |> send_resp(400, reason) 62 | end 63 | end 64 | 65 | defp prohibit_authenticated_session(conn, _opts) do 66 | if HAP.HAPSessionTransport.encrypted_session?() do 67 | conn 68 | |> send_resp(401, "Not Authorized") 69 | |> halt() 70 | else 71 | conn 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/hap/crypto/cha_cha_20.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.ChaCha20 do 2 | @moduledoc false 3 | # Functions to encrypt/tag and decrypt/verify using the chacha20_poly1305 cipher 4 | 5 | @type plaintext :: binary() 6 | @type ciphertext_with_authdata :: binary() 7 | @type key :: <<_::256>> 8 | @type nonce :: binary() 9 | @type aad :: binary() 10 | 11 | @doc """ 12 | Takes a binary containing encrypted data followed by a 16 byte tag, verifies the tag 13 | and decrypts the resultant data using the given key and nonce. Can take optional AAD 14 | data which is authenticated under the auth_tag but not encrypted. 15 | 16 | Returns `{:ok, plaintext}` or `{:error, message}` 17 | """ 18 | @spec decrypt_and_verify(ciphertext_with_authdata(), key(), nonce(), aad()) :: 19 | {:ok, plaintext()} | {:error, String.t()} 20 | def decrypt_and_verify(encrypted_data, key, nonce, aad \\ <<>>) do 21 | encrypted_data_length = byte_size(encrypted_data) - 16 22 | <> = encrypted_data 23 | 24 | case :crypto.crypto_one_time_aead(:chacha20_poly1305, key, pad(nonce), encrypted_data, aad, auth_tag, false) do 25 | :error -> {:error, "Message decryption error"} 26 | result -> {:ok, result} 27 | end 28 | end 29 | 30 | @doc """ 31 | Takes a plaintext binary and encrypts & tags it using the given key & nonce. Optionally takes 32 | AAD data which is authenticated under the auth tag but not included in the returned binary (it is 33 | up to the caller to convey the AAD to their counterparty). 34 | 35 | Returns `{:ok, encrypted_data <> auth_tag}` 36 | """ 37 | @spec encrypt_and_tag(plaintext(), key(), nonce(), aad()) :: {:ok, ciphertext_with_authdata()} 38 | def encrypt_and_tag(plaintext, key, nonce, aad \\ <<>>) do 39 | {encrypted_data, auth_tag} = 40 | :crypto.crypto_one_time_aead(:chacha20_poly1305, key, pad(nonce), plaintext, aad, true) 41 | 42 | {:ok, encrypted_data <> auth_tag} 43 | end 44 | 45 | @nonce_size 12 46 | 47 | defp pad(nonce) when byte_size(nonce) >= @nonce_size, do: nonce 48 | 49 | defp pad(nonce) do 50 | bits = (@nonce_size - byte_size(nonce)) * 8 51 | <<0::size(bits), nonce::binary>> 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/hap/iid.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.IID do 2 | @moduledoc false 3 | # Functions to map IIDs to/from service and characteristic indexes. 4 | # 5 | # Service & characteristic IDs are each encoded as 8 bits within the IID. This 6 | # makes it deterministic to extract the service and characteristic index from a given IID. 7 | # This provides 256 possible service indices & 255 possible characteristic indices, 8 | # which exceeds the limit of 100 services and characteristics as defined in 6.3.1 and 6.3.2 9 | # of the HAP specification. 10 | 11 | # Since IIDs are defined on the range [1, 18446744073709551615] per section 12 | # 2.6.1 of the HAP specification, we set the lowest bit of the IID to 1, this allowing for 13 | # service and characteristic indices of 0. As a consequence, any valid IID generated by 14 | # this module will be in the range [1, 65536]. We also add 1 to the indicated characateristic 15 | # index when populating the IID, reserving a characteristic index of 0 to represent service IIDs. 16 | # This arrangement has the property that the IID for a service index of 0 is encoded as a value of 1, 17 | # which the HAP protocol requires to represent the accessory information service. 18 | 19 | @doc false 20 | def service_index(iid) when iid in 1..65_536 do 21 | <> = <> 22 | {:ok, service_index} 23 | end 24 | 25 | @doc false 26 | def service_index(_iid), do: {:error, :invalid_iid} 27 | 28 | @doc false 29 | def characteristic_index(iid) when iid in 1..65_536 do 30 | <<_service_index::8, characteristic_index::8, 1::1>> = <> 31 | 32 | case characteristic_index do 33 | 0 -> {:error, :invalid_iid} 34 | result -> {:ok, result - 1} 35 | end 36 | end 37 | 38 | @doc false 39 | def characteristic_index(_iid), do: {:error, :invalid_iid} 40 | 41 | @doc false 42 | def to_iid(service_index) when service_index in 0..255 do 43 | <> = <> 44 | iid 45 | end 46 | 47 | @doc false 48 | def to_iid(service_index, characteristic_index) when service_index in 0..255 and characteristic_index in 0..255 do 49 | <> = <> 50 | iid 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/hap/services/television.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Services.Television do 2 | @moduledoc """ 3 | Struct representing an instance of the `public.hap.service.television` service 4 | 5 | The Television service is used to control Apple TV and other television accessories 6 | through HomeKit. It supports features like power control, input switching, volume 7 | control, and remote control functionality. 8 | """ 9 | 10 | defstruct active: nil, 11 | active_identifier: nil, 12 | configured_name: nil, 13 | sleep_discovery_mode: nil, 14 | brightness: nil, 15 | closed_captions: nil, 16 | display_order: nil, 17 | current_media_state: nil, 18 | target_media_state: nil, 19 | picture_mode: nil, 20 | power_mode_selection: nil, 21 | remote_key: nil, 22 | name: nil 23 | 24 | defimpl HAP.ServiceSource do 25 | def compile(value) do 26 | HAP.Service.ensure_required!(__MODULE__, "active", value.active) 27 | HAP.Service.ensure_required!(__MODULE__, "active_identifier", value.active_identifier) 28 | HAP.Service.ensure_required!(__MODULE__, "configured_name", value.configured_name) 29 | HAP.Service.ensure_required!(__MODULE__, "sleep_discovery_mode", value.sleep_discovery_mode) 30 | 31 | %HAP.Service{ 32 | type: "D8", 33 | characteristics: [ 34 | {HAP.Characteristics.Active, value.active}, 35 | {HAP.Characteristics.ActiveIdentifier, value.active_identifier}, 36 | {HAP.Characteristics.ConfiguredName, value.configured_name}, 37 | {HAP.Characteristics.SleepDiscoveryMode, value.sleep_discovery_mode}, 38 | {HAP.Characteristics.Brightness, value.brightness}, 39 | {HAP.Characteristics.ClosedCaptions, value.closed_captions}, 40 | {HAP.Characteristics.DisplayOrder, value.display_order}, 41 | {HAP.Characteristics.CurrentMediaState, value.current_media_state}, 42 | {HAP.Characteristics.TargetMediaState, value.target_media_state}, 43 | {HAP.Characteristics.PictureMode, value.picture_mode}, 44 | {HAP.Characteristics.PowerModeSelection, value.power_mode_selection}, 45 | {HAP.Characteristics.RemoteKey, value.remote_key}, 46 | {HAP.Characteristics.Name, value.name} 47 | ] 48 | } 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/hap/crypto/srp6a.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Crypto.SRP6A do 2 | @moduledoc false 3 | # Implements the various steps within the Stanford Remote Password (version 6a) flow 4 | 5 | import Bitwise 6 | 7 | def verifier(i, p) do 8 | s = :crypto.strong_rand_bytes(16) 9 | v = Strap.verifier(protocol(), i, p, s) 10 | {:ok, s, v} 11 | end 12 | 13 | def auth_context(v) do 14 | server = Strap.server(protocol(), v) 15 | {:ok, server, Strap.public_value(server)} 16 | end 17 | 18 | def client(username, password, salt) do 19 | client = Strap.client(protocol(), username, password, salt) 20 | {:ok, client, Strap.public_value(client)} 21 | end 22 | 23 | # TODO - this should go upstream. See https://github.com/twooster/strap/issues/2 24 | def shared_key({:server, _, _, _, _} = auth_context, a, i, s) do 25 | # Generate k 26 | {:ok, shared_key} = Strap.session_key(auth_context, a) 27 | k = shared_key |> HAP.Crypto.SHA512.hash() 28 | 29 | # Generate M_1 30 | # M_1 = H(H(N) xor H(g), H(I), s, A, B, K) 31 | {n, g} = prime_group() 32 | h_n = n |> HAP.Crypto.SHA512.hash() |> to_int 33 | h_g = g |> to_bin |> HAP.Crypto.SHA512.hash() |> to_int 34 | xor = bxor(h_n, h_g) |> to_bin 35 | h_i = i |> HAP.Crypto.SHA512.hash() 36 | b = auth_context |> Strap.public_value() 37 | m_1 = HAP.Crypto.SHA512.hash(xor <> h_i <> s <> a <> b <> k) 38 | 39 | # Generate M_2 40 | # M_2 = H(A, M_1, K) 41 | m_2 = HAP.Crypto.SHA512.hash(a <> m_1 <> k) 42 | 43 | {:ok, m_1, m_2, k} 44 | end 45 | 46 | def shared_key({:client, _, _, _, _} = auth_context, b, i, s) do 47 | # Generate k 48 | {:ok, shared_key} = Strap.session_key(auth_context, b) 49 | k = shared_key |> HAP.Crypto.SHA512.hash() 50 | 51 | # Generate M_1 52 | # M_1 = H(H(N) xor H(g), H(I), s, A, B, K) 53 | {n, g} = prime_group() 54 | h_n = n |> HAP.Crypto.SHA512.hash() |> to_int 55 | h_g = g |> to_bin |> HAP.Crypto.SHA512.hash() |> to_int 56 | xor = bxor(h_n, h_g) |> to_bin 57 | h_i = i |> HAP.Crypto.SHA512.hash() 58 | a = auth_context |> Strap.public_value() 59 | m_1 = HAP.Crypto.SHA512.hash(xor <> h_i <> s <> a <> b <> k) 60 | 61 | # Generate M_2 62 | # M_2 = H(A, M_1, K) 63 | m_2 = HAP.Crypto.SHA512.hash(a <> m_1 <> k) 64 | 65 | {:ok, m_1, m_2, k} 66 | end 67 | 68 | defp protocol do 69 | {n, g} = prime_group() 70 | Strap.protocol(:srp6a, n, g, :sha512) 71 | end 72 | 73 | defp prime_group do 74 | Strap.prime_group(3072) 75 | end 76 | 77 | defp to_bin(val) when is_integer(val), do: :binary.encode_unsigned(val) 78 | defp to_int(val) when is_bitstring(val), do: :binary.decode_unsigned(val) 79 | end 80 | -------------------------------------------------------------------------------- /lib/hap/accessory.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Accessory do 2 | @moduledoc """ 3 | Represents a single accessory object, containing a number of services 4 | """ 5 | 6 | defstruct name: "Generic HAP Accessory", 7 | model: "Generic HAP Model", 8 | manufacturer: "Generic HAP Manufacturer", 9 | serial_number: "Generic Serial Number", 10 | firmware_revision: "1.0", 11 | services: [] 12 | 13 | @typedoc """ 14 | Represents an accessory consisting of a number of services. Contains the following 15 | fields: 16 | 17 | * `name`: The name to assign to this accessory, for example 'Ceiling Fan' 18 | * `model`: The model name to assign to this accessory, for example 'FanCo Whisper III' 19 | * `manufacturer`: The manufacturer of this accessory, for example 'FanCo' 20 | * `serial_number`: The serial number of this accessory, for example '0012345' 21 | * `firmware_revision`: The firmware revision of this accessory, for example '1.0' 22 | * `services`: A list of services to include in this accessory 23 | """ 24 | @type t :: %__MODULE__{ 25 | name: name(), 26 | model: model(), 27 | manufacturer: manufacturer(), 28 | serial_number: serial_number(), 29 | firmware_revision: firmware_revision(), 30 | services: [HAP.Service.t()] 31 | } 32 | 33 | @typedoc """ 34 | The name to advertise for this accessory, for example 'HAP Light Bulb' 35 | """ 36 | @type name :: String.t() 37 | 38 | @typedoc """ 39 | The model of this accessory, for example 'HAP Light Bulb Supreme' 40 | """ 41 | @type model :: String.t() 42 | 43 | @typedoc """ 44 | The manufacturer of this accessory, for example 'HAP Co.' 45 | """ 46 | @type manufacturer :: String.t() 47 | 48 | @typedoc """ 49 | The serial number of this accessory, for example '0012345' 50 | """ 51 | @type serial_number :: String.t() 52 | 53 | @typedoc """ 54 | The firmware recvision of this accessory, for example '1.0' or '1.0.1' 55 | """ 56 | @type firmware_revision :: String.t() 57 | 58 | @doc false 59 | def compile(%__MODULE__{services: services} = accessory) do 60 | all_services = 61 | [%HAP.Services.AccessoryInformation{accessory: accessory}, %HAP.Services.ProtocolInformation{}] ++ 62 | services 63 | 64 | %__MODULE__{ 65 | services: all_services |> Enum.map(&HAP.Service.compile/1) 66 | } 67 | end 68 | 69 | @doc false 70 | def get_service(%__MODULE__{services: services}, iid) do 71 | with {:ok, service_index} <- HAP.IID.service_index(iid), 72 | %HAP.Service{} = service <- Enum.at(services, service_index) do 73 | {:ok, service} 74 | else 75 | _ -> {:error, -70_409} 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/hap/characteristic_definition.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.CharacteristicDefinition do 2 | @moduledoc """ 3 | A behaviour which encapsulates the functinos required to define a characteristic. 4 | At runtime, characteristics are modeled via the `HAP.Characteristic` struct which 5 | contains the runtime values for the characteristic itself, as well as metadata about 6 | the characteristic. A `HAP.CharacteristicDefinition` is used to provide the template 7 | values for these fields. HAP contains definitions for many common HomeKit characteristics 8 | already, and users may define other characteristics by providing an implementation of 9 | this behaviour as the first value in the characteristic definition tuple in a service. 10 | """ 11 | 12 | @typedoc """ 13 | The type of a characteristic as defined in Section 6.6.1 of Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 14 | """ 15 | @type type :: String.t() 16 | 17 | @typedoc """ 18 | A permission of a characteristic as defined in Table 6.4 of Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 19 | One of `pr`, `pw`, `ev`, `aa`, `tw`, `hd`, or `wr` 20 | """ 21 | @type perm :: String.t() 22 | 23 | @typedoc """ 24 | The format of a characteristic as defined in Table 6.5 of Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 25 | One of `bool`, `uint8`, `uint16`, `uint32`, `uint64`, `int`, `float`, `string`, `tlv8`, or `data` 26 | """ 27 | @type format :: String.t() 28 | 29 | @typedoc """ 30 | The unit of measure of a characrteristic's value 31 | """ 32 | @type unit :: any() 33 | 34 | @doc """ 35 | The HomeKit type code for this characteristic 36 | """ 37 | @callback type :: type() 38 | 39 | @doc """ 40 | The permissions to allow for this characteristic 41 | """ 42 | @callback perms :: [perm()] 43 | 44 | @doc """ 45 | The format of this characteristic's data 46 | """ 47 | @callback format :: format() 48 | 49 | @doc """ 50 | The minimum value allowed for this characteristic's value 51 | """ 52 | @callback min_value :: HAP.Characteristic.value() 53 | 54 | @doc """ 55 | The maximum value allowed for this characteristic's value 56 | """ 57 | @callback max_value :: HAP.Characteristic.value() 58 | 59 | @doc """ 60 | The step size by which this characteristic's value may change 61 | """ 62 | @callback step_value :: HAP.Characteristic.value() 63 | 64 | @doc """ 65 | The units of this Characteristic's value 66 | """ 67 | @callback units :: unit() 68 | 69 | @doc """ 70 | Whether or not to only return values via events. Required mostly to satisfy the somewhat oddball *Programmable Switch Event* 71 | characteristic as defined in section 9.75 of Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 72 | """ 73 | @callback event_only :: boolean() 74 | 75 | @optional_callbacks min_value: 0, max_value: 0, step_value: 0, units: 0, event_only: 0 76 | end 77 | -------------------------------------------------------------------------------- /test/hap/hap_session_transport_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.HAPSessionTransportTest do 2 | use ExUnit.Case 3 | 4 | test "can send in the clear & upgrade a live socket to encrypted transport" do 5 | {:ok, listener_socket} = HAP.HAPSessionTransport.listen(0, skip_registration: true) 6 | {:ok, {_ip, port}} = HAP.HAPSessionTransport.sockname(listener_socket) 7 | 8 | server_task = 9 | Task.async(fn -> 10 | {:ok, server_socket} = HAP.HAPSessionTransport.accept(listener_socket) 11 | 12 | {:ok, data} = HAP.HAPSessionTransport.recv(server_socket, 0, :infinity) 13 | HAP.HAPSessionTransport.send(server_socket, data) 14 | 15 | HAP.HAPSessionTransport.put_send_key(<<1>>) 16 | HAP.HAPSessionTransport.put_recv_key(<<2>>) 17 | 18 | {:ok, data} = HAP.HAPSessionTransport.recv(server_socket, 0, :infinity) 19 | HAP.HAPSessionTransport.send(server_socket, data) 20 | 21 | {:ok, data} = HAP.HAPSessionTransport.recv(server_socket, 0, :infinity) 22 | HAP.HAPSessionTransport.send(server_socket, data) 23 | 24 | HAP.HAPSessionTransport.close(server_socket) 25 | end) 26 | 27 | {:ok, client_socket} = :gen_tcp.connect(:localhost, port, mode: :binary, active: false) 28 | 29 | :ok = HAP.HAPSessionTransport.send(client_socket, <<1, 2, 3>>) 30 | assert {:ok, <<1, 2, 3>>} == HAP.HAPSessionTransport.recv(client_socket, 0, :infinity) 31 | 32 | refute HAP.HAPSessionTransport.encrypted_session?() 33 | 34 | # Note that these are reversed since we're acting as the controller here 35 | HAP.HAPSessionTransport.put_send_key(<<2>>) 36 | HAP.HAPSessionTransport.put_recv_key(<<1>>) 37 | 38 | assert HAP.HAPSessionTransport.encrypted_session?() 39 | 40 | :ok = HAP.HAPSessionTransport.send(client_socket, <<1, 2, 3>>) 41 | assert {:ok, <<1, 2, 3>>} == HAP.HAPSessionTransport.recv(client_socket, 0, :infinity) 42 | 43 | :ok = HAP.HAPSessionTransport.send(client_socket, <<1, 2, 3>>) 44 | assert {:ok, <<1, 2, 3>>} == HAP.HAPSessionTransport.recv(client_socket, 0, :infinity) 45 | 46 | Task.await(server_task) 47 | end 48 | 49 | test "transmits encrypted over the wire when so configured" do 50 | {:ok, listener_socket} = HAP.HAPSessionTransport.listen(0, skip_registration: true) 51 | {:ok, {_ip, port}} = HAP.HAPSessionTransport.sockname(listener_socket) 52 | 53 | server_task = 54 | Task.async(fn -> 55 | {:ok, server_socket} = HAP.HAPSessionTransport.accept(listener_socket) 56 | 57 | HAP.HAPSessionTransport.put_send_key(<<1>>) 58 | HAP.HAPSessionTransport.put_recv_key(<<2>>) 59 | 60 | HAP.HAPSessionTransport.send(server_socket, <<1, 2, 3>>) 61 | 62 | HAP.HAPSessionTransport.close(server_socket) 63 | end) 64 | 65 | # Connect using a raw TCP socket since we want to test the actual wire 66 | {:ok, client_socket} = :gen_tcp.connect(:localhost, port, mode: :binary, active: false) 67 | 68 | {encrypted_data, auth_tag} = 69 | :crypto.crypto_one_time_aead( 70 | :chacha20_poly1305, 71 | <<1>>, 72 | <<0::32, 0::integer-size(64)-little>>, 73 | <<1, 2, 3>>, 74 | <<3::integer-size(16)-little>>, 75 | true 76 | ) 77 | 78 | # Read using raw TCP socket to see *exactly* what is on the wire 79 | assert {:ok, <<3::integer-size(16)-little, encrypted_data::binary-3, auth_tag::binary-16>>} == 80 | :gen_tcp.recv(client_socket, 0, :infinity) 81 | 82 | Task.await(server_task) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/hap/encrypted_http_server.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.EncryptedHTTPServer do 2 | @moduledoc false 3 | # Defines the HTTP interface for a HomeKit Accessory which may only be accessed over a secure channel 4 | 5 | use Plug.Router 6 | 7 | plug(:match) 8 | plug(:require_authenticated_session) 9 | plug(:dispatch) 10 | 11 | def init(opts) do 12 | opts 13 | end 14 | 15 | post "/pairings" do 16 | pair_state = HAP.HAPSessionTransport.get_pair_state() 17 | 18 | HAP.Pairings.handle_message(conn.body_params, pair_state) 19 | |> case do 20 | {:ok, response} -> 21 | conn 22 | |> put_resp_header("content-type", "application/pairing+tlv8") 23 | |> send_resp(200, HAP.TLVEncoder.to_binary(response)) 24 | 25 | {:error, reason} -> 26 | conn 27 | |> send_resp(400, reason) 28 | end 29 | end 30 | 31 | get "/accessories" do 32 | response = HAP.AccessoryServerManager.get_accessories() 33 | 34 | conn 35 | |> put_resp_header("content-type", "application/hap+json") 36 | |> send_resp(200, Jason.encode!(response)) 37 | end 38 | 39 | get "/characteristics" do 40 | opts = 41 | conn.params 42 | |> Map.take(~w[meta perms type ev]) 43 | |> Enum.filter(fn {_k, v} -> v == "1" end) 44 | |> Enum.map(fn {k, _v} -> String.to_atom(k) end) 45 | 46 | characteristics = 47 | conn.params["id"] 48 | |> String.split(",") 49 | |> Enum.map(&String.split(&1, ".")) 50 | |> Enum.map(fn [aid, iid] -> %{aid: String.to_integer(aid), iid: String.to_integer(iid)} end) 51 | |> HAP.AccessoryServerManager.get_characteristics(:pr, opts) 52 | 53 | if Enum.all?(characteristics, fn %{status: status} -> status == 0 end) do 54 | characteristics = characteristics |> Enum.map(fn characteristic -> characteristic |> Map.delete(:status) end) 55 | 56 | conn 57 | |> put_resp_header("content-type", "application/hap+json") 58 | |> send_resp(200, Jason.encode!(%{characteristics: characteristics})) 59 | else 60 | conn 61 | |> put_resp_header("content-type", "application/hap+json") 62 | |> send_resp(207, Jason.encode!(%{characteristics: characteristics})) 63 | end 64 | end 65 | 66 | put "/characteristics" do 67 | characteristics = 68 | conn.body_params["characteristics"] 69 | |> HAP.AccessoryServerManager.put_characteristics() 70 | 71 | all_success = Enum.all?(characteristics, fn %{status: status} -> status == 0 end) 72 | no_values = Enum.all?(characteristics, fn result -> !Map.has_key?(result, :value) end) 73 | 74 | if all_success && no_values do 75 | conn 76 | |> put_resp_header("content-type", "application/hap+json") 77 | |> send_resp(204, "") 78 | else 79 | conn 80 | |> put_resp_header("content-type", "application/hap+json") 81 | |> send_resp(207, Jason.encode!(%{characteristics: characteristics})) 82 | end 83 | end 84 | 85 | # Note that we do not enforce timed writes per 6.7.2.4; we just throw this request on the ground 86 | put "/prepare" do 87 | conn 88 | |> put_resp_header("content-type", "application/hap+json") 89 | |> send_resp(200, Jason.encode!(%{status: 0})) 90 | end 91 | 92 | defp require_authenticated_session(conn, _opts) do 93 | if HAP.HAPSessionTransport.encrypted_session?() do 94 | conn 95 | else 96 | conn 97 | |> send_resp(401, "Not Authorized") 98 | |> halt() 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/hap/pair_setup_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.PairSetupTest do 2 | use ExUnit.Case, async: false 3 | 4 | setup do 5 | {:ok, _pid} = HAP.Test.TestAccessoryServer.test_server() |> start_supervised() 6 | 7 | :ok 8 | end 9 | 10 | test "a valid pair-setup flow results in a pairing being made" do 11 | # A very quick & dirty implementation of the iOS side of the Pair-Setup flow 12 | 13 | # Build our request parameters 14 | port = HAP.AccessoryServerManager.port() 15 | {:ok, client} = HAP.Test.HTTPClient.init(:localhost, port) 16 | endpoint = "/pair-setup" 17 | 18 | # Build M1 19 | m1 = %{0x06 => <<1>>, 0x00 => <<0>>} 20 | 21 | # Send M1 22 | {:ok, 200, headers, body} = 23 | HAP.Test.HTTPClient.post(client, endpoint, HAP.TLVEncoder.to_binary(m1), 24 | "content-type": "application/pairing+tlv8" 25 | ) 26 | 27 | assert Keyword.get(headers, :"content-type") == "application/pairing+tlv8" 28 | 29 | # Verify M2 & Build M3 30 | _m2 = %{0x06 => <<2>>, 0x03 => b, 0x02 => salt} = HAP.TLVParser.parse_tlv(body) 31 | {:ok, auth_context, a} = HAP.Crypto.SRP6A.client("Pair-Setup", HAP.AccessoryServerManager.pairing_code(), salt) 32 | {:ok, m_1, m_2, session_key} = HAP.Crypto.SRP6A.shared_key(auth_context, b, "Pair-Setup", salt) 33 | m3 = %{0x06 => <<3>>, 0x03 => a, 0x04 => m_1} 34 | 35 | # Send M3 36 | {:ok, 200, headers, body} = 37 | HAP.Test.HTTPClient.post(client, endpoint, HAP.TLVEncoder.to_binary(m3), 38 | "content-type": "application/pairing+tlv8" 39 | ) 40 | 41 | assert Keyword.get(headers, :"content-type") == "application/pairing+tlv8" 42 | 43 | # Verify M4 & Build M5 44 | _m4 = %{0x06 => <<4>>, 0x04 => ^m_2} = HAP.TLVParser.parse_tlv(body) 45 | {:ok, ios_ltpk, ios_ltsk} = HAP.Crypto.EDDSA.key_gen() 46 | 47 | {:ok, ios_device_x} = 48 | HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info") 49 | 50 | ios_identifier = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" 51 | ios_device_info = ios_device_x <> ios_identifier <> ios_ltpk 52 | {:ok, ios_signature} = HAP.Crypto.EDDSA.sign(ios_device_info, ios_ltsk) 53 | sub_tlv = %{0x01 => ios_identifier, 0x03 => ios_ltpk, 0x0A => ios_signature} 54 | {:ok, envelope_key} = HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") 55 | 56 | {:ok, encrypted_data_and_tag} = 57 | HAP.Crypto.ChaCha20.encrypt_and_tag(HAP.TLVEncoder.to_binary(sub_tlv), envelope_key, "PS-Msg05") 58 | 59 | m5 = %{0x06 => <<5>>, 0x05 => encrypted_data_and_tag} 60 | 61 | # Send M5 62 | {:ok, 200, headers, body} = 63 | HAP.Test.HTTPClient.post(client, endpoint, HAP.TLVEncoder.to_binary(m5), 64 | "content-type": "application/pairing+tlv8" 65 | ) 66 | 67 | assert Keyword.get(headers, :"content-type") == "application/pairing+tlv8" 68 | 69 | # Verify M6 70 | _m6 = %{0x06 => <<6>>, 0x05 => encrypted_data} = HAP.TLVParser.parse_tlv(body) 71 | {:ok, sub_tlv} = HAP.Crypto.ChaCha20.decrypt_and_verify(encrypted_data, envelope_key, "PS-Msg06") 72 | accessory_identifier = HAP.AccessoryServerManager.identifier() 73 | accessory_ltpk = HAP.AccessoryServerManager.ltpk() 74 | 75 | %{0x01 => ^accessory_identifier, 0x03 => ^accessory_ltpk, 0x0A => accessory_signature} = 76 | HAP.TLVParser.parse_tlv(sub_tlv) 77 | 78 | {:ok, accessory_x} = 79 | HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info") 80 | 81 | accessory_info = accessory_x <> accessory_identifier <> accessory_ltpk 82 | {:ok, true} = HAP.Crypto.EDDSA.verify(accessory_info, accessory_signature, accessory_ltpk) 83 | 84 | # Finally, verify that we have successfully paired on the Accessory Server side 85 | assert HAP.AccessoryServerManager.controller_pairing(ios_identifier) == {ios_ltpk, <<1>>} 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/hap/value_store.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.ValueStore do 2 | @moduledoc """ 3 | Defines the behaviour required of a module that wishes to act as the backing data store 4 | for a given HomeKit characteristic 5 | 6 | # Simple Value Store 7 | 8 | To implement a value store for a simple value whose value does not change asynchronously, 9 | you must implement the `c:get_value/1` and `c:put_value/2` callbacks. These callbacks each 10 | take a set of opts (specified in the initial configuration passed to `HAP.start_link/1`) to 11 | allow your implementation to discriminate between various values within the same `HAP.ValueStore` 12 | module. 13 | 14 | # Supporting Asynchronous Notifications 15 | 16 | To support notifying HomeKit of changes to an accessory's characteristics (such as a user pressing 17 | a button or a flood sensor detecting water), implementations of `HAP.ValueStore` may choose to 18 | implement the optional `c:set_change_token/2` callback. This callback will provide your implementation 19 | with a change token to use when notifying HAP of changes to the corresponding value. To notify HAP of 20 | changes to a value, pass this change token to the `HAP.value_changed/1` function. HAP will then query 21 | your value store for the new value of the corresponding characteristic and notify any HomeKit controllers 22 | of the change. 23 | 24 | There are a number of things to be aware of when using Asynchronous Notifications: 25 | 26 | * Your value store must be prepared to answer calls to `c:get_value/1` with the updated value before 27 | calling `HAP.value_changed/1`. 28 | * Do not call `HAP.value_changed/1` to notify HAP of changes which come from HAP itself (ie: do not call it in the course 29 | of implementing `c:put_value/2`). Use it only for notifying HAP of changes which are truly asynchronous. 30 | * If you have not yet received a `c:set_change_token/2` call, then you should not call `HAP.value_changed/1`; HAP will only 31 | provide you with a change token for characteristics which a HomeKit controller has requested notifications on. Specifically, 32 | do not retain change tokens between runs; they should maintain the same lifetime as the underlying HAP process. 33 | * The call to `HAP.value_changed/1` is guaranteed to return quickly. It does no work beyond casting a message 34 | to HAP to set the notification process in motion. 35 | """ 36 | 37 | @type t :: module() 38 | @type opts :: keyword() 39 | @opaque change_token :: {term(), term()} 40 | 41 | @doc """ 42 | Return the value of a value hosted by this value store. The passed list of opts 43 | is as specified in the hosting `HAP.Configuration` and can be used to distinguish a 44 | particular value within a larger value store (perhaps by GPIO pin or similar) 45 | 46 | Returns the value stored by this value store 47 | """ 48 | @callback get_value(opts :: opts()) :: {:ok, HAP.Characteristic.value()} | {:error, String.t()} 49 | 50 | @doc """ 51 | 52 | Sets the value of a value hosted by this value store. The passed list of opts 53 | is as specified in the hosting `HAP.Configuration` and can be used to distinguish a 54 | particular value within a larger value store (perhaps by GPIO pin or similar) 55 | 56 | Returns `:ok` or `{:error, reason}` 57 | """ 58 | @callback put_value(value :: HAP.Characteristic.value(), opts :: opts()) :: :ok | {:error, String.t()} 59 | 60 | @doc """ 61 | 62 | Informs the value store of the change token to use when notifying HAP of asynchronous 63 | changes to the value in this store. This token should be provided to `HAP.value_changed/1` as the 64 | sole argument; HAP will make a subsequent call to `c:get_value/1` to obtain the changed value 65 | 66 | Returns `:ok` or `{:error, reason}` 67 | """ 68 | @callback set_change_token(change_token :: change_token(), opts :: opts()) :: :ok | {:error, String.t()} 69 | 70 | @optional_callbacks set_change_token: 2 71 | end 72 | -------------------------------------------------------------------------------- /lib/hap/characteristic.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Characteristic do 2 | @moduledoc """ 3 | Functions to aid in the manipulation of characteristics tuples 4 | """ 5 | 6 | @typedoc """ 7 | Represents a single characteristic consisting of a static definition and a value source 8 | """ 9 | @type t :: {module(), value_source()} 10 | 11 | @typedoc """ 12 | Represents a source for a characteristic value. May be either a static literal or 13 | a `{mod, opts}` tuple which is consulted when reading / writing a characteristic 14 | """ 15 | @type value_source :: value() | {HAP.ValueStore.t(), value_opts: HAP.ValueStore.opts()} 16 | 17 | @typedoc """ 18 | The resolved value of a characteristic 19 | """ 20 | @type value :: any() 21 | 22 | @doc false 23 | def get_type({characteristic_definition, _value_source}) do 24 | characteristic_definition.type() 25 | end 26 | 27 | @doc false 28 | def get_perms({characteristic_definition, _value_source}) do 29 | characteristic_definition.perms() 30 | end 31 | 32 | @doc false 33 | def get_format({characteristic_definition, _value_source}) do 34 | characteristic_definition.format() 35 | end 36 | 37 | @doc false 38 | def get_meta({characteristic_definition, _value_source}) do 39 | [ 40 | {:format, :format}, 41 | {:minValue, :min_value}, 42 | {:maxValue, :max_value}, 43 | {:minStep, :step_value}, 44 | {:unit, :unit}, 45 | {:maxLength, :max_length} 46 | ] 47 | |> Enum.reduce(%{}, fn {return_key, call_key}, acc -> 48 | if function_exported?(characteristic_definition, call_key, 0) do 49 | Map.put(acc, return_key, apply(characteristic_definition, call_key, [])) 50 | else 51 | acc 52 | end 53 | end) 54 | end 55 | 56 | @doc false 57 | def get_value({characteristic_definition, value_source}, :pr) do 58 | if "pr" in characteristic_definition.perms() do 59 | if function_exported?(characteristic_definition, :event_only, 0) && characteristic_definition.event_only() do 60 | {:ok, nil} 61 | else 62 | get_value_from_source(value_source) 63 | end 64 | else 65 | {:error, -70_405} 66 | end 67 | end 68 | 69 | def get_value({characteristic_definition, value_source}, :ev) do 70 | if "ev" in characteristic_definition.perms() do 71 | get_value_from_source(value_source) 72 | else 73 | {:error, -70_405} 74 | end 75 | end 76 | 77 | @doc false 78 | def get_value!(characteristic, disposition) do 79 | {:ok, value} = get_value(characteristic, disposition) 80 | value 81 | end 82 | 83 | @doc false 84 | def put_value({characteristic_definition, value_source}, value) do 85 | if "pw" in characteristic_definition.perms() do 86 | value = maybe_cast_value({characteristic_definition, value_source}, value) 87 | put_value_to_source(value_source, value) 88 | else 89 | {:error, -70_404} 90 | end 91 | end 92 | 93 | defp maybe_cast_value(characteristic, value) do 94 | case get_type(characteristic) do 95 | "25" -> value in [true, 1] 96 | _other_type -> value 97 | end 98 | end 99 | 100 | @doc false 101 | def set_change_token({_characteristic_definition, {mod, opts}}, token) do 102 | if function_exported?(mod, :set_change_token, 2) do 103 | mod.set_change_token(token, opts) 104 | else 105 | {:error, -70_406} 106 | end 107 | end 108 | 109 | def set_change_token({_characteristic_definition, _value_source}, _token) do 110 | raise "Cannot set change token on a statically defined characteristic" 111 | end 112 | 113 | defp get_value_from_source({mod, opts}) do 114 | mod.get_value(opts) 115 | end 116 | 117 | defp get_value_from_source(value) do 118 | {:ok, value} 119 | end 120 | 121 | defp put_value_to_source({mod, opts}, value) do 122 | mod.put_value(value, opts) 123 | end 124 | 125 | defp put_value_to_source(_value, _new_value) do 126 | raise "Cannot write to a statically defined characteristic" 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/hap/pair_verify.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.PairVerify do 2 | @moduledoc false 3 | # Implements the Pair Verify flow described in Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 4 | 5 | require Logger 6 | 7 | # We intentionally structure our constant names to match those in the HAP specification 8 | # credo:disable-for-this-file Credo.Check.Readability.ModuleAttributeNames 9 | # credo:disable-for-this-file Credo.Check.Readability.VariableNames 10 | 11 | @kTLVType_Identifier 0x01 12 | @kTLVType_PublicKey 0x03 13 | @kTLVType_EncryptedData 0x05 14 | @kTLVType_State 0x06 15 | @kTLVType_Error 0x07 16 | @kTLVType_Signature 0x0A 17 | 18 | @kTLVError_Authentication <<0x02>> 19 | 20 | @kFlag_Admin <<0x01>> 21 | 22 | def init do 23 | %{step: 1} 24 | end 25 | 26 | @doc false 27 | # Handles `` messages and returns `` messages 28 | def handle_message(%{@kTLVType_State => <<1>>, @kTLVType_PublicKey => ios_epk}, %{step: 1}) do 29 | {:ok, accessory_epk, accessory_esk} = HAP.Crypto.ECDH.key_gen() 30 | {:ok, session_key} = HAP.Crypto.ECDH.compute_key(ios_epk, accessory_esk) 31 | accessory_info = accessory_epk <> HAP.AccessoryServerManager.identifier() <> ios_epk 32 | {:ok, accessory_signature} = HAP.Crypto.EDDSA.sign(accessory_info, HAP.AccessoryServerManager.ltsk()) 33 | 34 | resp_sub_tlv = 35 | %{ 36 | @kTLVType_Identifier => HAP.AccessoryServerManager.identifier(), 37 | @kTLVType_Signature => accessory_signature 38 | } 39 | |> HAP.TLVEncoder.to_binary() 40 | 41 | {:ok, hashed_k} = HAP.Crypto.HKDF.generate(session_key, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info") 42 | {:ok, encrypted_data_and_tag} = HAP.Crypto.ChaCha20.encrypt_and_tag(resp_sub_tlv, hashed_k, "PV-Msg02") 43 | 44 | response = %{ 45 | @kTLVType_State => <<2>>, 46 | @kTLVType_PublicKey => accessory_epk, 47 | @kTLVType_EncryptedData => encrypted_data_and_tag 48 | } 49 | 50 | {:ok, response, %{step: 3, session_key: session_key, ios_epk: ios_epk, accessory_epk: accessory_epk}, nil, nil} 51 | end 52 | 53 | # Handles `` messages and returns `` messages 54 | def handle_message(%{@kTLVType_State => <<3>>, @kTLVType_EncryptedData => encrypted_data_and_tag}, %{ 55 | step: 3, 56 | session_key: session_key, 57 | ios_epk: ios_epk, 58 | accessory_epk: accessory_epk 59 | }) do 60 | with {:ok, hashed_k} <- 61 | HAP.Crypto.HKDF.generate(session_key, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info"), 62 | {:ok, tlv} <- HAP.Crypto.ChaCha20.decrypt_and_verify(encrypted_data_and_tag, hashed_k, "PV-Msg03"), 63 | %{@kTLVType_Identifier => ios_identifier, @kTLVType_Signature => ios_signature} <- 64 | HAP.TLVParser.parse_tlv(tlv), 65 | ios_device_info <- ios_epk <> ios_identifier <> accessory_epk, 66 | {ios_ltpk, ios_permissions} <- HAP.AccessoryServerManager.controller_pairing(ios_identifier), 67 | admin? <- ios_permissions == @kFlag_Admin, 68 | {:ok, true} <- HAP.Crypto.EDDSA.verify(ios_device_info, ios_signature, ios_ltpk), 69 | {:ok, accessory_to_controller_key} <- 70 | HAP.Crypto.HKDF.generate(session_key, "Control-Salt", "Control-Read-Encryption-Key"), 71 | {:ok, controller_to_accessory_key} <- 72 | HAP.Crypto.HKDF.generate(session_key, "Control-Salt", "Control-Write-Encryption-Key") do 73 | Logger.info("Verified session for controller #{ios_identifier}") 74 | 75 | {:ok, %{@kTLVType_State => <<4>>}, %{ios_ltpk: ios_ltpk, admin?: admin?}, accessory_to_controller_key, 76 | controller_to_accessory_key} 77 | else 78 | _ -> 79 | Logger.error("Pair-Verify Error") 80 | 81 | {:ok, %{@kTLVType_State => <<4>>, @kTLVType_Error => @kTLVError_Authentication}, 82 | %{step: 4, session_key: session_key}, nil, nil} 83 | end 84 | end 85 | 86 | def handle_message(tlv, state) do 87 | Logger.error("Pair-Verify Received unexpected message: #{inspect(tlv)}, state: #{inspect(state)}") 88 | 89 | {:error, "Unexpected message for pairing state"} 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/hap/accessories_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HAP.AcessoriesTest do 2 | use ExUnit.Case, async: false 3 | 4 | setup do 5 | {:ok, _pid} = start_supervised(HAP.Test.TestValueStore) 6 | HAP.Test.TestValueStore.put_value(true, value_name: :lightbulb) 7 | 8 | {:ok, _pid} = 9 | [ 10 | accessories: [ 11 | %HAP.Accessory{ 12 | services: [ 13 | %HAP.Services.LightBulb{on: {HAP.Test.TestValueStore, value_name: :lightbulb}} 14 | ] 15 | } 16 | ] 17 | ] 18 | |> HAP.Test.TestAccessoryServer.test_server() 19 | |> start_supervised() 20 | 21 | port = HAP.AccessoryServerManager.port() 22 | {:ok, client} = HAP.Test.HTTPClient.init(:localhost, port) 23 | 24 | {:ok, %{client: client}} 25 | end 26 | 27 | describe "GET /accessories" do 28 | test "it should return the expected accessories tree", context do 29 | # Setup an encrypted session 30 | :ok = HAP.Test.HTTPClient.setup_encrypted_session(context.client) 31 | 32 | {:ok, 200, headers, body} = HAP.Test.HTTPClient.get(context.client, "/accessories") 33 | 34 | assert Keyword.get(headers, :"content-type") == "application/hap+json" 35 | 36 | assert Jason.decode!(body) == %{ 37 | "accessories" => [ 38 | %{ 39 | "aid" => 1, 40 | "services" => [ 41 | %{ 42 | "iid" => 1, 43 | "type" => "3E", 44 | "characteristics" => [ 45 | %{ 46 | "format" => "string", 47 | "iid" => 3, 48 | "perms" => ["pr"], 49 | "type" => "23", 50 | "value" => "Generic HAP Accessory" 51 | }, 52 | %{ 53 | "format" => "string", 54 | "iid" => 5, 55 | "perms" => ["pr"], 56 | "type" => "21", 57 | "value" => "Generic HAP Model" 58 | }, 59 | %{ 60 | "format" => "string", 61 | "iid" => 7, 62 | "perms" => ["pr"], 63 | "type" => "20", 64 | "value" => "Generic HAP Manufacturer" 65 | }, 66 | %{ 67 | "format" => "string", 68 | "iid" => 9, 69 | "perms" => ["pr"], 70 | "type" => "30", 71 | "value" => "Generic Serial Number" 72 | }, 73 | %{"format" => "string", "iid" => 11, "perms" => ["pr"], "type" => "52", "value" => "1.0"}, 74 | %{"format" => "bool", "iid" => 13, "perms" => ["pw"], "type" => "14"} 75 | ] 76 | }, 77 | %{ 78 | "iid" => 513, 79 | "type" => "A2", 80 | "characteristics" => [ 81 | %{"format" => "string", "iid" => 515, "perms" => ["pr"], "type" => "37", "value" => "1.1.0"} 82 | ] 83 | }, 84 | %{ 85 | "iid" => 1025, 86 | "type" => "43", 87 | "characteristics" => [ 88 | %{ 89 | "format" => "bool", 90 | "iid" => 1027, 91 | "perms" => ["pr", "pw", "ev"], 92 | "type" => "25", 93 | "value" => true 94 | } 95 | ] 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | end 102 | 103 | test "it should require an authenticated session", context do 104 | {:ok, 401, _headers, _body} = HAP.Test.HTTPClient.get(context.client, "/accessories") 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/hap/pairings.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Pairings do 2 | @moduledoc false 3 | # Implements the Add / Remove / List Pairings flows described in Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 4 | 5 | require Logger 6 | 7 | # We intentionally structure our constant names to match those in the HAP specification 8 | # credo:disable-for-this-file Credo.Check.Readability.ModuleAttributeNames 9 | # credo:disable-for-this-file Credo.Check.Readability.VariableNames 10 | 11 | @kTLVType_Method 0x00 12 | @kTLVType_Identifier 0x01 13 | @kTLVType_PublicKey 0x03 14 | @kTLVType_State 0x06 15 | @kTLVType_Error 0x07 16 | @kTLVType_Permissions 0x0B 17 | @kTLVType_Separator 0xFF 18 | 19 | @kTLVError_Unknown <<0x01>> 20 | @kTLVError_Authentication <<0x02>> 21 | 22 | @kMethod_AddPairing <<0x03>> 23 | @kMethod_RemovePairing <<0x04>> 24 | @kMethod_ListPairings <<0x05>> 25 | 26 | @doc false 27 | # Handles Add Pairing `` messages and returns `` messages 28 | def handle_message( 29 | %{ 30 | @kTLVType_State => <<1>>, 31 | @kTLVType_Method => @kMethod_AddPairing, 32 | @kTLVType_Identifier => additional_ios_identifier, 33 | @kTLVType_PublicKey => additional_ios_ltpk, 34 | @kTLVType_Permissions => additional_ios_permissions 35 | }, 36 | %{admin?: true} 37 | ) do 38 | case HAP.AccessoryServerManager.controller_pairing(additional_ios_identifier) do 39 | nil -> 40 | Logger.info( 41 | "Adding controller #{additional_ios_identifier} with permissions #{inspect(additional_ios_permissions)}" 42 | ) 43 | 44 | HAP.AccessoryServerManager.add_controller_pairing( 45 | additional_ios_identifier, 46 | additional_ios_ltpk, 47 | additional_ios_permissions 48 | ) 49 | 50 | {:ok, %{@kTLVType_State => <<2>>}} 51 | 52 | {^additional_ios_ltpk, _existing_ios_permissions} -> 53 | Logger.info( 54 | "Updating controller #{additional_ios_identifier} with permissions #{inspect(additional_ios_permissions)}" 55 | ) 56 | 57 | HAP.AccessoryServerManager.add_controller_pairing( 58 | additional_ios_identifier, 59 | additional_ios_ltpk, 60 | additional_ios_permissions 61 | ) 62 | 63 | {:ok, %{@kTLVType_State => <<2>>}} 64 | 65 | _ -> 66 | Logger.error("AddPairing Existing controller LTPK does not match") 67 | {:ok, %{@kTLVType_State => <<2>>, @kTLVType_Error => @kTLVError_Unknown}} 68 | end 69 | end 70 | 71 | # Handles Remove Pairing `` messages and returns `` messages 72 | # TODO - this should tear down its own session 73 | def handle_message( 74 | %{ 75 | @kTLVType_State => <<1>>, 76 | @kTLVType_Method => @kMethod_RemovePairing, 77 | @kTLVType_Identifier => removed_ios_identifier 78 | }, 79 | %{admin?: true} 80 | ) do 81 | Logger.info("Removed pairing with controller #{removed_ios_identifier}") 82 | 83 | {:ok, removed_last_pairing} = HAP.AccessoryServerManager.remove_controller_pairing(removed_ios_identifier) 84 | 85 | if removed_last_pairing do 86 | HAP.Discovery.reload() 87 | HAP.Display.update_pairing_info_display() 88 | end 89 | 90 | {:ok, %{@kTLVType_State => <<2>>}} 91 | end 92 | 93 | # Handles List Pairings `` messages and returns `` messages 94 | def handle_message(%{@kTLVType_State => <<1>>, @kTLVType_Method => @kMethod_ListPairings}, %{admin?: true}) do 95 | response = 96 | HAP.AccessoryServerManager.controller_pairings() 97 | |> Enum.map_intersperse([{@kTLVType_Separator, <<>>}], fn {ios_identifer, {ios_ltpk, ios_permissions}} -> 98 | [ 99 | {@kTLVType_Identifier, ios_identifer}, 100 | {@kTLVType_PublicKey, ios_ltpk}, 101 | {@kTLVType_Permissions, ios_permissions} 102 | ] 103 | end) 104 | |> Enum.flat_map(& &1) 105 | 106 | {:ok, Enum.concat([{@kTLVType_State, <<2>>}], response)} 107 | end 108 | 109 | def handle_message(%{@kTLVType_State => <<1>>}, %{admin?: false}) do 110 | Logger.error("Pairing Requesting controller is not an admin") 111 | response = %{@kTLVType_State => <<2>>, @kTLVType_Error => @kTLVError_Authentication} 112 | {:ok, response} 113 | end 114 | 115 | def handle_message(tlv, state) do 116 | Logger.error("Pairing Received unexpected message: #{inspect(tlv)}, state: #{inspect(state)}") 117 | {:error, "Unexpected message for pairing state"} 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![HAP](https://user-images.githubusercontent.com/79646/67910894-dd4dc280-fb5a-11e9-9ca9-4be6633cc1a6.png) 2 | 3 | [![Build Status](https://github.com/mtrudel/hap/workflows/Elixir%20CI/badge.svg)](https://github.com/mtrudel/hap/actions) 4 | [![Docs](https://img.shields.io/badge/api-docs-green.svg?style=flat)](https://hexdocs.pm/hap) 5 | [![Hex.pm](https://img.shields.io/hexpm/v/hap.svg?style=flat&color=blue)](https://hex.pm/packages/hap) 6 | 7 | HAP is a framework for building DIY HomeKit accessories based on Apple's [HomeKit Accessory Protocol](https://developer.apple.com/homekit/) specification. 8 | You can think of it as [homebridge](https://github.com/nfarina/homebridge) for Elixir (with a bit more of a focus on 9 | building actual accessories via Nerves) in contrast to Homebridge's typical use as a bridge to existing accessories. 10 | 11 | As shown in the [HAP Demo](https://github.com/mtrudel/hap_demo) project, integrating HAP support into an existing Elixir 12 | project is extremely straightforward - all that is required in most cases is to define the services and characteristics 13 | you wish to expose, and to provide an implementation of `HAP.ValueStore` for each non-static characteristic you define. 14 | 15 | In many cases, integrating with HAP can be as simple as: 16 | 17 | ```elixir 18 | accessory_server = 19 | %HAP.AccessoryServer{ 20 | name: "My HAP Demo Device", 21 | model: "HAP Demo Device", 22 | identifier: "11:22:33:44:12:66", 23 | accessory_type: 5, 24 | accessories: [ 25 | %HAP.Accessory{ 26 | name: "My HAP Lightbulb", 27 | services: [ 28 | %HAP.Services.LightBulb{on: {MyApp.Lightbulb, gpio_pin: 23}} 29 | ] 30 | } 31 | ] 32 | } 33 | 34 | children = [{HAP, accessory_server}] 35 | 36 | Supervisor.start_link(children, opts) 37 | 38 | ... 39 | ``` 40 | 41 | ## Supported Services & Characteristics 42 | 43 | As originally developed, HAP included a fairly small set of services & characteristics (mostly due to the author's 44 | laziness & the immediate need for only a handful of the ~45 services & ~128 characteristics defined in the 45 | specification). However, it is quite easy to add definitions for new services & characteristics, and PRs to add such 46 | definitions are extremely welcome. The [lightbulb service](https://github.com/mtrudel/hap/blob/main/lib/hap/services/light_bulb.ex) 47 | is a complete implementation of a service and serves as an excellent starting point for creating your own. You can consult 48 | sections 8 and 9 of the [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/) to determine 49 | what characteristics are required and optional for a given service. Note that only implementations of public services and 50 | characteristics as defined in the HomeKit specification will be considered for inclusion in HAP. 51 | 52 | ### Asynchronous Change Notifications 53 | 54 | HAP supports notifications (as defined in section 6.8 of the [HomeKit Accessory Protocol 55 | Specification](https://developer.apple.com/homekit/)). This allows your accessory to notify HomeKit of changes which 56 | happen asynchronously, such as a user pushing a button on the accessory, or a sensor detecting a water leak. To send 57 | such notifications, your `HAP.ValueStore` implementation must support the `c:HAP.ValueStore.set_change_token/2` 58 | callback. Consult the `HAP.ValueStore` documentation for more detail. 59 | 60 | ## Known Issues 61 | 62 | * No support for dynamically updating the services advertised by a HAP instance (this is slated for HAP 2.0) 63 | * Incomplete support for tearing down existing sessions on pairing removal (this is slated for HAP 2.0) 64 | * No support for HomeKit Secure Video / RTP (support is not currently planned, but PRs are of course welcome) 65 | * Timed write support is supported, but timeouts are not enforced 66 | 67 | In addition, there may well be bugs or gaps in functionality not listed above. If you encounter any, please feel free 68 | to file an issue. 69 | 70 | ## Installation 71 | 72 | HAP is available in Hex. The package can be installed by adding hap to your list of dependencies in mix.exs: 73 | 74 | ``` 75 | def deps do 76 | [ 77 | {:hap, "~> 0.4"} 78 | ] 79 | end 80 | ``` 81 | 82 | HAP is intended to be used within a host application which provides concrete implementations for various HomeKit 83 | characteristics. Check out the [HAP Demo](https://github.com/mtrudel/hap_demo) app for an example of how to use HAP. 84 | 85 | Documentation can be found at https://hexdocs.pm/hap/. 86 | 87 | Note that in order to have access to the required crypto methods for HAP to function, OTP 23 or newer is required. Also note that OTP 25.0.x has a [defect](https://github.com/erlang/otp/issues/6313) that breaks HAP (fixed in OTP 25.1 and newer). 88 | 89 | ## License 90 | 91 | MIT 92 | -------------------------------------------------------------------------------- /test/support/test_http_client.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.Test.HTTPClient do 2 | @moduledoc """ 3 | A super simple HTTP client that knows how to speak HAP encrypted sessions. Not 4 | even remotely generally compliant. 5 | """ 6 | 7 | def init(host, port) do 8 | :gen_tcp.connect(host, port, mode: :binary, active: false) 9 | end 10 | 11 | def get(socket, path, headers \\ []) do 12 | request(socket, "GET", path, "", headers) 13 | end 14 | 15 | def put(socket, path, body, headers \\ []) do 16 | request(socket, "PUT", path, body, headers) 17 | end 18 | 19 | def post(socket, path, body, headers \\ []) do 20 | request(socket, "POST", path, body, headers) 21 | end 22 | 23 | def request(socket, method, path, body, headers) do 24 | {:ok, {_ip, port}} = HAP.HAPSessionTransport.sockname(socket) 25 | 26 | request = [ 27 | "#{method} #{path} HTTP/1.1\r\n", 28 | Enum.map(headers, fn {k, v} -> "#{k}: #{v}\r\n" end), 29 | ["connection: keep-alive\r\n"], 30 | ["host: localhost:#{port}\r\n"], 31 | ["content-length: #{byte_size(body)}\r\n"], 32 | "\r\n", 33 | body 34 | ] 35 | 36 | HAP.HAPSessionTransport.send(socket, request) 37 | {:ok, result} = HAP.HAPSessionTransport.recv(socket, 0, :infinity) 38 | 39 | ["HTTP/1.1" <> code | lines] = result |> String.split("\r\n") 40 | 41 | {code, _text} = code |> String.trim() |> Integer.parse() 42 | 43 | {headers, [_ | body]} = 44 | lines 45 | |> Enum.split_while(fn line -> line != "" end) 46 | 47 | headers = 48 | headers 49 | |> Enum.map(fn header -> 50 | [k, v] = header |> String.split(":", parts: 2) 51 | {k |> String.trim() |> String.to_atom(), String.trim(v)} 52 | end) 53 | 54 | {:ok, code, headers, IO.iodata_to_binary(body)} 55 | end 56 | 57 | defdelegate encrypted_session?, to: HAP.HAPSessionTransport 58 | 59 | def setup_encrypted_session(client, permissions \\ <<1>>) do 60 | # Set ourselves up as if we'd already set up a pairing 61 | ios_identifier = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" 62 | {:ok, ios_ltpk, ios_ltsk} = HAP.Crypto.EDDSA.key_gen() 63 | HAP.AccessoryServerManager.add_controller_pairing(ios_identifier, ios_ltpk, permissions) 64 | 65 | # A very quick & dirty implementation of the iOS side of the Pair-Verify flow 66 | # 67 | endpoint = "/pair-verify" 68 | 69 | # Build M1 70 | {:ok, ios_epk, ios_esk} = HAP.Crypto.ECDH.key_gen() 71 | m1 = %{0x06 => <<1>>, 0x03 => ios_epk} 72 | 73 | # Send M1 74 | {:ok, 200, headers, body} = 75 | post(client, endpoint, HAP.TLVEncoder.to_binary(m1), "content-type": "application/pairing+tlv8") 76 | 77 | "application/pairing+tlv8" = Keyword.get(headers, :"content-type") 78 | 79 | # Verify M2 & Build M3 80 | _m2 = %{0x06 => <<2>>, 0x03 => accessory_epk, 0x05 => encrypted_data} = HAP.TLVParser.parse_tlv(body) 81 | {:ok, session_key} = HAP.Crypto.ECDH.compute_key(accessory_epk, ios_esk) 82 | {:ok, envelope_key} = HAP.Crypto.HKDF.generate(session_key, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info") 83 | {:ok, sub_tlv} = HAP.Crypto.ChaCha20.decrypt_and_verify(encrypted_data, envelope_key, "PV-Msg02") 84 | accessory_identifier = HAP.AccessoryServerManager.identifier() 85 | %{0x01 => ^accessory_identifier, 0x0A => accessory_signature} = HAP.TLVParser.parse_tlv(sub_tlv) 86 | accessory_device_info = accessory_epk <> accessory_identifier <> ios_epk 87 | accessory_ltpk = HAP.AccessoryServerManager.ltpk() 88 | {:ok, true} = HAP.Crypto.EDDSA.verify(accessory_device_info, accessory_signature, accessory_ltpk) 89 | ios_device_info = ios_epk <> ios_identifier <> accessory_epk 90 | {:ok, ios_signature} = HAP.Crypto.EDDSA.sign(ios_device_info, ios_ltsk) 91 | sub_tlv = %{0x01 => ios_identifier, 0x0A => ios_signature} 92 | 93 | {:ok, encrypted_data_and_tag} = 94 | HAP.Crypto.ChaCha20.encrypt_and_tag(HAP.TLVEncoder.to_binary(sub_tlv), envelope_key, "PV-Msg03") 95 | 96 | m3 = %{0x06 => <<3>>, 0x05 => encrypted_data_and_tag} 97 | 98 | # Send M3 99 | {:ok, 200, headers, body} = 100 | post(client, endpoint, HAP.TLVEncoder.to_binary(m3), "content-type": "application/pairing+tlv8") 101 | 102 | "application/pairing+tlv8" = Keyword.get(headers, :"content-type") 103 | 104 | # Verify M4 105 | _m4 = %{0x06 => <<4>>} = HAP.TLVParser.parse_tlv(body) 106 | 107 | {:ok, accessory_to_controller_key} = 108 | HAP.Crypto.HKDF.generate(session_key, "Control-Salt", "Control-Read-Encryption-Key") 109 | 110 | {:ok, controller_to_accessory_key} = 111 | HAP.Crypto.HKDF.generate(session_key, "Control-Salt", "Control-Write-Encryption-Key") 112 | 113 | # Note that these are reversed since we're acting as the controller here 114 | HAP.HAPSessionTransport.put_send_key(controller_to_accessory_key) 115 | HAP.HAPSessionTransport.put_recv_key(accessory_to_controller_key) 116 | 117 | :ok 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/hap/hap_session_transport.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.HAPSessionTransport do 2 | @moduledoc false 3 | # Implements cleartext TCP transport with optional chacha20_poly1305 encryption 4 | # as mandated by section 6.5.2 of the HomeKit Accessory Protocol specification 5 | 6 | @behaviour ThousandIsland.Transport 7 | 8 | @pair_state_key :pair_state_key 9 | @send_key_key :hap_send_key 10 | @recv_key_key :hap_recv_key 11 | 12 | def get_pair_state, do: Process.get(@pair_state_key, HAP.PairVerify.init()) 13 | def put_pair_state(new_state), do: Process.put(@pair_state_key, new_state) 14 | def put_send_key(send_key), do: Process.put(@send_key_key, send_key) 15 | def put_recv_key(recv_key), do: Process.put(@recv_key_key, recv_key) 16 | 17 | @impl ThousandIsland.Transport 18 | def listen(port, options) do 19 | {hap_options, options} = Keyword.split(options, [:skip_registration]) 20 | 21 | with {:ok, listener_socket} <- ThousandIsland.Transports.TCP.listen(port, options) do 22 | unless Keyword.get(hap_options, :skip_registration) do 23 | {:ok, {_ip, port}} = :inet.sockname(listener_socket) 24 | HAP.AccessoryServerManager.set_port(port) 25 | HAP.Discovery.reload() 26 | end 27 | 28 | {:ok, listener_socket} 29 | end 30 | end 31 | 32 | @impl ThousandIsland.Transport 33 | defdelegate accept(listener_socket), to: ThousandIsland.Transports.TCP 34 | 35 | @impl ThousandIsland.Transport 36 | defdelegate handshake(socket), to: ThousandIsland.Transports.TCP 37 | 38 | @impl ThousandIsland.Transport 39 | defdelegate upgrade(socket, options), to: ThousandIsland.Transports.TCP 40 | 41 | @impl ThousandIsland.Transport 42 | defdelegate controlling_process(socket, pid), to: ThousandIsland.Transports.TCP 43 | 44 | @impl ThousandIsland.Transport 45 | def recv(socket, length, timeout) do 46 | case ThousandIsland.Transports.TCP.recv(socket, length, timeout) do 47 | {:ok, data} -> decrypt_if_needed(data) 48 | other -> other 49 | end 50 | end 51 | 52 | @impl ThousandIsland.Transport 53 | def send(socket, data) do 54 | case Process.get(@send_key_key) do 55 | nil -> 56 | ThousandIsland.Transports.TCP.send(socket, data) 57 | 58 | send_key -> 59 | with counter <- Process.get(:send_counter, 0), 60 | nonce <- pad_counter(counter), 61 | length_aad <- <>, 62 | {:ok, encrypted_data_and_tag} <- 63 | HAP.Crypto.ChaCha20.encrypt_and_tag(data, send_key, nonce, length_aad) do 64 | Process.put(:send_counter, counter + 1) 65 | ThousandIsland.Transports.TCP.send(socket, length_aad <> encrypted_data_and_tag) 66 | end 67 | end 68 | end 69 | 70 | @impl ThousandIsland.Transport 71 | defdelegate sendfile(socket, filename, offset, length), to: ThousandIsland.Transports.TCP 72 | 73 | @impl ThousandIsland.Transport 74 | defdelegate getopts(socket, options), to: ThousandIsland.Transports.TCP 75 | 76 | @impl ThousandIsland.Transport 77 | defdelegate setopts(socket, options), to: ThousandIsland.Transports.TCP 78 | 79 | @impl ThousandIsland.Transport 80 | defdelegate shutdown(socket, way), to: ThousandIsland.Transports.TCP 81 | 82 | @impl ThousandIsland.Transport 83 | defdelegate close(socket), to: ThousandIsland.Transports.TCP 84 | 85 | @impl ThousandIsland.Transport 86 | defdelegate sockname(socket), to: ThousandIsland.Transports.TCP 87 | 88 | @impl ThousandIsland.Transport 89 | defdelegate peername(socket), to: ThousandIsland.Transports.TCP 90 | 91 | @impl ThousandIsland.Transport 92 | defdelegate peercert(socket), to: ThousandIsland.Transports.TCP 93 | 94 | @impl ThousandIsland.Transport 95 | defdelegate connection_information(socket), to: ThousandIsland.Transports.TCP 96 | 97 | @impl ThousandIsland.Transport 98 | defdelegate secure?(), to: ThousandIsland.Transports.TCP 99 | 100 | @impl ThousandIsland.Transport 101 | defdelegate getstat(socket), to: ThousandIsland.Transports.TCP 102 | 103 | @impl ThousandIsland.Transport 104 | defdelegate negotiated_protocol(socket), to: ThousandIsland.Transports.TCP 105 | 106 | def decrypt_if_needed(<>) do 107 | case Process.get(@recv_key_key) do 108 | nil -> 109 | {:ok, packet} 110 | 111 | recv_key -> 112 | with <> <- packet, 113 | <> <- packet, 114 | counter <- Process.get(:recv_counter, 0), 115 | nonce <- pad_counter(counter) do 116 | Process.put(:recv_counter, counter + 1) 117 | HAP.Crypto.ChaCha20.decrypt_and_verify(encrypted_data <> tag, recv_key, nonce, length_aad) 118 | end 119 | end 120 | end 121 | 122 | def encrypted_session? do 123 | !is_nil(Process.get(@recv_key_key)) && !is_nil(Process.get(@send_key_key)) 124 | end 125 | 126 | defp pad_counter(counter) do 127 | <<0::32, counter::integer-size(64)-little>> 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/hap/accessory_server_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.AccessoryServerManager do 2 | @moduledoc false 3 | # Holds the top-level state of a HAP instance, most notably a HAP.AccessoryServer struct 4 | 5 | use GenServer 6 | 7 | require Logger 8 | 9 | def start_link(config) do 10 | GenServer.start_link(__MODULE__, config, name: __MODULE__) 11 | end 12 | 13 | @doc false 14 | def config_number, do: HAP.PersistentStorage.get(:config_number) 15 | 16 | @doc false 17 | def ltpk do 18 | {ltpk, _} = HAP.PersistentStorage.get(:ltk) 19 | ltpk 20 | end 21 | 22 | @doc false 23 | def ltsk do 24 | {_, ltsk} = HAP.PersistentStorage.get(:ltk) 25 | ltsk 26 | end 27 | 28 | @doc false 29 | def port(pid \\ __MODULE__), do: GenServer.call(pid, :get_port) 30 | 31 | @doc false 32 | def set_port(port, pid \\ __MODULE__), do: GenServer.call(pid, {:put_port, port}) 33 | 34 | @doc false 35 | def display_module(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :display_module}) 36 | 37 | @doc false 38 | def name(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :name}) 39 | 40 | @doc false 41 | def model(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :model}) 42 | 43 | @doc false 44 | def identifier(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :identifier}) 45 | 46 | @doc false 47 | def accessory_type(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :accessory_type}) 48 | 49 | @doc false 50 | def pairing_code(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :pairing_code}) 51 | 52 | @doc false 53 | def setup_id(pid \\ __MODULE__), do: GenServer.call(pid, {:get, :setup_id}) 54 | 55 | @doc false 56 | def pairing_url(pid \\ __MODULE__), do: GenServer.call(pid, :get_pairing_url) 57 | 58 | @doc false 59 | def paired?(pid \\ __MODULE__), do: GenServer.call(pid, :paired?) 60 | 61 | @doc false 62 | def controller_pairings(pid \\ __MODULE__), do: GenServer.call(pid, :controller_pairings) 63 | 64 | @doc false 65 | def controller_pairing(ios_identifier, pid \\ __MODULE__) do 66 | GenServer.call(pid, {:controller_pairing, ios_identifier}) 67 | end 68 | 69 | @doc false 70 | def add_controller_pairing(ios_identifier, ios_ltpk, permissions, pid \\ __MODULE__) do 71 | GenServer.call(pid, {:add_controller_pairing, ios_identifier, ios_ltpk, permissions}) 72 | end 73 | 74 | @doc false 75 | def remove_controller_pairing(ios_identifier, pid \\ __MODULE__) do 76 | GenServer.call(pid, {:remove_controller_pairing, ios_identifier}) 77 | end 78 | 79 | @doc false 80 | def get_accessories(pid \\ __MODULE__) do 81 | GenServer.call(pid, :get_accessories) 82 | end 83 | 84 | @doc false 85 | def get_characteristics(characteristics, disposition, opts \\ [], pid \\ __MODULE__) do 86 | GenServer.call(pid, {:get_characteristics, characteristics, disposition, opts}) 87 | end 88 | 89 | @doc false 90 | def put_characteristics(characteristics, pid \\ __MODULE__) do 91 | GenServer.call(pid, {:put_characteristics, characteristics}) 92 | end 93 | 94 | @doc false 95 | def value_changed(value_token, pid \\ __MODULE__) do 96 | GenServer.cast(pid, {:value_changed, value_token}) 97 | end 98 | 99 | def init(%HAP.AccessoryServer{} = accessory_server) do 100 | HAP.PersistentStorage.put_new_lazy(:config_number, fn -> 1 end) 101 | HAP.PersistentStorage.put_new_lazy(:pairings, fn -> %{} end) 102 | 103 | HAP.PersistentStorage.put_new_lazy(:ltk, fn -> 104 | {:ok, ltpk, ltsk} = HAP.Crypto.EDDSA.key_gen() 105 | {ltpk, ltsk} 106 | end) 107 | 108 | old_config_hash = HAP.PersistentStorage.get(:config_hash, 0) 109 | new_config_hash = HAP.AccessoryServer.config_hash(accessory_server) 110 | 111 | if old_config_hash != new_config_hash do 112 | Logger.info("Configuration has changed; incrementing config number") 113 | HAP.PersistentStorage.get_and_update(:config_number, &{:ok, &1 + 1}) 114 | end 115 | 116 | HAP.PersistentStorage.put(:config_hash, new_config_hash) 117 | {:ok, %{accessory_server: accessory_server, port: 0}} 118 | end 119 | 120 | def handle_call(:get_port, _from, state) do 121 | {:reply, state[:port], state} 122 | end 123 | 124 | def handle_call({:put_port, port}, _from, state) do 125 | {:reply, :ok, %{state | port: port}} 126 | end 127 | 128 | def handle_call(:get_pairing_url, _from, state) do 129 | {:reply, HAP.AccessoryServer.pairing_url(state[:accessory_server]), state} 130 | end 131 | 132 | def handle_call({:get, param}, _from, state) do 133 | {:reply, Map.get(state[:accessory_server], param), state} 134 | end 135 | 136 | def handle_call(:paired?, _from, state) do 137 | {:reply, HAP.PersistentStorage.get(:pairings) != %{}, state} 138 | end 139 | 140 | def handle_call(:controller_pairings, _from, state) do 141 | {:reply, HAP.PersistentStorage.get(:pairings), state} 142 | end 143 | 144 | def handle_call({:controller_pairing, ios_identifier}, _from, state) do 145 | {:reply, HAP.PersistentStorage.get(:pairings)[ios_identifier], state} 146 | end 147 | 148 | def handle_call({:add_controller_pairing, ios_identifier, ios_ltpk, permissions}, _from, state) do 149 | pairing_state_changed = 150 | HAP.PersistentStorage.get_and_update( 151 | :pairings, 152 | &{&1 == %{}, Map.put(&1, ios_identifier, {ios_ltpk, permissions})} 153 | ) 154 | 155 | {:reply, pairing_state_changed, state} 156 | end 157 | 158 | def handle_call({:remove_controller_pairing, ios_identifier}, _from, state) do 159 | pairing_state_changed = 160 | HAP.PersistentStorage.get_and_update(:pairings, fn pairings -> 161 | new_map = Map.delete(pairings, ios_identifier) 162 | {new_map == %{}, new_map} 163 | end) 164 | 165 | {:reply, pairing_state_changed, state} 166 | end 167 | 168 | def handle_call(:get_accessories, _from, state) do 169 | response = HAP.AccessoryServer.accessories_tree(state[:accessory_server]) 170 | {:reply, response, state} 171 | end 172 | 173 | def handle_call({:get_characteristics, characteristics, disposition, opts}, _from, state) do 174 | response = HAP.AccessoryServer.get_characteristics(state[:accessory_server], characteristics, disposition, opts) 175 | {:reply, response, state} 176 | end 177 | 178 | def handle_call({:put_characteristics, characteristics}, {from, _}, state) do 179 | response = HAP.AccessoryServer.put_characteristics(state[:accessory_server], characteristics, from) 180 | {:reply, response, state} 181 | end 182 | 183 | def handle_cast({:value_changed, {aid, iid}}, state) do 184 | HAP.AccessoryServer.value_changed(state[:accessory_server], %{aid: aid, iid: iid}) 185 | {:noreply, state} 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/hap.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP do 2 | @moduledoc """ 3 | HAP is an implementation of the [HomeKit Accessory Protocol](https://developer.apple.com/homekit/) specification. 4 | It allows for the creation of Elixir powered HomeKit accessories which can be controlled from a user's 5 | iOS device in a similar manner to commercially available HomeKit accessories such as light bulbs, window 6 | coverings and other smart home accessories. 7 | 8 | ## The HomeKit Data Model 9 | 10 | The data model of the HomeKit Accessory Protocol is represented as a tree structure. At the top level, a single HAP 11 | instance represents an *Accessory Server*. An accessory server hosts one or more *Accessory Objects*. Each accessory object 12 | represents a single, discrete physical accessory. In the case of directly connected devices, an accessory server typically 13 | hosts a single accessory object which represents device itself, whereas bridges will have one accessory object for each discrete 14 | physical object which they bridge to. Within HAP, an accessory server is represented by a `HAP.AccessoryServer` struct, and 15 | an accessory by the `HAP.Accessory` struct. 16 | 17 | Each accessory object contains exposes a set of *Services*, each of which represents a unit of functionality. As an 18 | example, a HomeKit accessory server which represented a ceiling fan with a light would contain one accessory object 19 | called 'Ceiling Fan', which would contain two services each representing the light and the fan. In addition to user-visible 20 | services, each accessory exposes an Accessory Information Service which contains information about the service's name, 21 | manufacturer, serial number and other properties. Within HAP, a service is represented by a `HAP.Service` struct. 22 | 23 | A service is made up of one or more *Characteristics*, each of which represents a specific aspect of the given service. 24 | For example, a light bulb service exposes an On Characteristic, which is a boolean value reflecting the current on or 25 | off state of the light. If it is a dimmable light, it may also expose a Brightness Characteristic. If it is a color 26 | changing light, it may also expose a Hue Characteristic. Within HAP, a characteristic is represented by a tuple of a 27 | `HAP.CharacteristicDefinition` and a value source. 28 | 29 | ## Using HAP 30 | 31 | HAP provides a high-level interface to the HomeKit Accessory Protocol, allowing an application to 32 | present any number of accessories to an iOS HomeKit controller. HAP is intended to be embedded within a host 33 | application which is responsible for providing the actual backing implementations of the various characteristics 34 | exposed via HomeKit. These are provided to HAP in the form of `HAP.ValueStore` implementations. For example, consider 35 | a Nerves application which exposes itself to HomeKit as a light bulb. Assume that the actual physical control of the 36 | light is controlled by GPIO pin 23. A typical configuration of HAP would look something like this: 37 | 38 | ```elixir 39 | accessory_server = 40 | %HAP.AccessoryServer{ 41 | name: "My HAP Demo Device", 42 | model: "HAP Demo Device", 43 | identifier: "11:22:33:44:12:66", 44 | accessory_type: 5, 45 | accessories: [ 46 | %HAP.Accessory{ 47 | name: "My HAP Lightbulb", 48 | services: [ 49 | %HAP.Services.LightBulb{on: {MyApp.Lightbulb, gpio_pin: 23}} 50 | ] 51 | } 52 | ] 53 | } 54 | 55 | children = [{HAP, accessory_server}] 56 | 57 | Supervisor.start_link(children, opts) 58 | 59 | ... 60 | ``` 61 | 62 | In this example, the application developer is responsible for creating a `MyApp.Lightbulb` module which implements the `HAP.ValueStore` 63 | behaviour. This module would be called by HAP whenever it needs to change or query the current state of the light. The 64 | extra options (`gpio_pin: 23` in the above example) are conveyed to this module on every call, allowing a single value store 65 | implementation to service any number of characteristics or services. 66 | 67 | HAP provides structs to represent the most common services, such as light bulbs, switches, and other common device types. 68 | HAP compiles these structs into generic `HAP.Service` structs when starting up, based on each source struct's implementation 69 | of the `HAP.ServiceSource` protocol. This allows for expressive definition of services by the application developer, while 70 | providing for less boilerplate within HAP itself. For users who wish to create additional device types not defined in 71 | HAP, users may define their accessories in terms of low-level `HAP.Service` and `HAP.CharacteristicDefinition` structs. For more 72 | information, consult the type definitions for `t:HAP.AccessoryServer.t/0`, `t:HAP.Accessory.t/0`, `t:HAP.Service.t/0`, 73 | `t:HAP.Characteristic.t/0`, and the `HAP.CharacteristicDefinition` behaviour. 74 | """ 75 | 76 | use Supervisor 77 | 78 | @doc """ 79 | Starts a HAP instance based on the passed config 80 | """ 81 | @spec start_link(HAP.AccessoryServer.t()) :: Supervisor.on_start() 82 | def start_link(config) do 83 | Supervisor.start_link(__MODULE__, config) 84 | end 85 | 86 | def init(%HAP.AccessoryServer{} = accessory_server) do 87 | accessory_server = accessory_server |> HAP.AccessoryServer.compile() 88 | 89 | children = [ 90 | {HAP.PersistentStorage, accessory_server.data_path}, 91 | {HAP.AccessoryServerManager, accessory_server}, 92 | HAP.EventManager, 93 | HAP.PairSetup, 94 | {Bandit, 95 | plug: HAP.HTTPServer, 96 | port: 0, 97 | http_1_options: [clear_process_dict: false], 98 | thousand_island_options: [handler_module: HAP.HAPSessionHandler, transport_module: HAP.HAPSessionTransport]} 99 | ] 100 | 101 | Supervisor.init(children, strategy: :rest_for_one) 102 | end 103 | 104 | @doc """ 105 | Called by user applications whenever a characteristic value has changed. The change token is passed to `HAP.ValueStore` 106 | instances via the `c:HAP.ValueStore.set_change_token/2` callback. 107 | """ 108 | @spec value_changed(HAP.ValueStore.change_token()) :: :ok 109 | defdelegate value_changed(change_token), to: HAP.AccessoryServerManager 110 | 111 | @doc """ 112 | Resets all local state for the given server. Removes all key & state information and allows the 113 | server to be paired anew. 114 | """ 115 | @spec reset(pid()) :: :ok 116 | def reset(server_pid) do 117 | storage_pid = 118 | Supervisor.which_children(server_pid) 119 | |> List.keyfind(HAP.PersistentStorage, 0) 120 | |> elem(1) 121 | 122 | HAP.PersistentStorage.clear(storage_pid) 123 | Process.exit(storage_pid, :restart) 124 | :ok 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/hap/pair_setup.ex: -------------------------------------------------------------------------------- 1 | defmodule HAP.PairSetup do 2 | @moduledoc false 3 | # Implements the Pair Setup flow described in Apple's [HomeKit Accessory Protocol Specification](https://developer.apple.com/homekit/). 4 | # Since Pair Setup is a singleton operation, this is implemented as a named GenServer 5 | 6 | use GenServer 7 | 8 | require Logger 9 | 10 | # We intentionally structure our constant names to match those in the HAP specification 11 | # credo:disable-for-this-file Credo.Check.Readability.ModuleAttributeNames 12 | # credo:disable-for-this-file Credo.Check.Readability.VariableNames 13 | 14 | @i "Pair-Setup" 15 | @kTLVType_Method 0x00 16 | @kTLVType_Identifier 0x01 17 | @kTLVType_Salt 0x02 18 | @kTLVType_PublicKey 0x03 19 | @kTLVType_Proof 0x04 20 | @kTLVType_EncryptedData 0x05 21 | @kTLVType_State 0x06 22 | @kTLVType_Error 0x07 23 | @kTLVType_Signature 0x0A 24 | 25 | @kTLVError_Authentication <<0x02>> 26 | @kTLVError_Unavailable <<0x06>> 27 | @kTLVError_Busy <<0x07>> 28 | 29 | @kFlag_Admin <<0x01>> 30 | 31 | def start_link(opts) do 32 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 33 | end 34 | 35 | @doc false 36 | def handle_message(message, pid \\ __MODULE__) do 37 | GenServer.call(pid, message) 38 | end 39 | 40 | def init(_opts) do 41 | HAP.Display.update_pairing_info_display() 42 | {:ok, %{step: 1}} 43 | end 44 | 45 | # Handles `` messages and returns `` messages 46 | def handle_call(%{@kTLVType_State => <<1>>, @kTLVType_Method => <<0>>}, _from, %{step: 1} = state) do 47 | if HAP.AccessoryServerManager.paired?() do 48 | Logger.error("Pair-Setup Already paired") 49 | response = %{@kTLVType_State => <<2>>, @kTLVType_Error => @kTLVError_Unavailable} 50 | {:reply, {:ok, response}, state} 51 | else 52 | p = HAP.AccessoryServerManager.pairing_code() 53 | {:ok, s, v} = HAP.Crypto.SRP6A.verifier(@i, p) 54 | {:ok, auth_context, b} = HAP.Crypto.SRP6A.auth_context(v) 55 | 56 | response = %{@kTLVType_State => <<2>>, @kTLVType_PublicKey => b, @kTLVType_Salt => s} 57 | {:reply, {:ok, response}, %{step: 3, auth_context: auth_context, salt: s}} 58 | end 59 | end 60 | 61 | def handle_call(%{@kTLVType_State => <<1>>, @kTLVType_Method => <<0>>}, _from, state) do 62 | Logger.error("Pair-Setup Already pairing") 63 | response = %{@kTLVType_State => <<2>>, @kTLVType_Error => @kTLVError_Busy} 64 | {:reply, {:ok, response}, state} 65 | end 66 | 67 | # Handles `` messages and returns `` messages 68 | def handle_call( 69 | %{@kTLVType_State => <<3>>, @kTLVType_PublicKey => a, @kTLVType_Proof => proof}, 70 | _from, 71 | %{step: 3, auth_context: auth_context, salt: s} = state 72 | ) do 73 | {:ok, m_1, m_2, k} = HAP.Crypto.SRP6A.shared_key(auth_context, a, @i, s) 74 | 75 | if proof == m_1 do 76 | response = %{@kTLVType_State => <<4>>, @kTLVType_Proof => m_2} 77 | {:reply, {:ok, response}, %{step: 5, session_key: k}} 78 | else 79 | Logger.error("Pair-Setup Provided proof does not match") 80 | response = %{@kTLVType_State => <<4>>, @kTLVType_Error => @kTLVError_Authentication} 81 | {:reply, {:ok, response}, state} 82 | end 83 | end 84 | 85 | # Handles `` messages and returns `` messages 86 | # 87 | # Note that the specifics of deriving the envelope key are not described in R1 or R2 of the HAP specification; 88 | # guidance was taken from other third-party implementations for the specific key material to use 89 | # 90 | def handle_call( 91 | %{@kTLVType_State => <<5>>, @kTLVType_EncryptedData => encrypted_data}, 92 | _from, 93 | %{step: 5, session_key: session_key} = state 94 | ) do 95 | with {:ok, envelope_key} <- 96 | HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info"), 97 | {:ok, tlv} <- HAP.Crypto.ChaCha20.decrypt_and_verify(encrypted_data, envelope_key, "PS-Msg05"), 98 | {:ok, ios_identifier, ios_ltpk} <- extract_ios_device_exchange(tlv, session_key), 99 | {:ok, response_sub_tlv} <- 100 | build_accessory_device_exchange( 101 | HAP.AccessoryServerManager.identifier(), 102 | HAP.AccessoryServerManager.ltpk(), 103 | HAP.AccessoryServerManager.ltsk(), 104 | session_key 105 | ), 106 | {:ok, encrypted_response} <- HAP.Crypto.ChaCha20.encrypt_and_tag(response_sub_tlv, envelope_key, "PS-Msg06") do 107 | HAP.AccessoryServerManager.add_controller_pairing(ios_identifier, ios_ltpk, @kFlag_Admin) 108 | 109 | Logger.info("Successfully paired with controller #{ios_identifier}") 110 | 111 | HAP.Discovery.reload() 112 | 113 | response = %{ 114 | @kTLVType_State => <<6>>, 115 | @kTLVType_EncryptedData => encrypted_response 116 | } 117 | 118 | {:reply, {:ok, response}, %{step: 1}} 119 | else 120 | {:error, reason} -> 121 | Logger.error("Pair-Setup Encountered error: #{reason}") 122 | response = %{@kTLVType_State => <<6>>, @kTLVType_Error => @kTLVError_Authentication} 123 | {:reply, {:ok, response}, state} 124 | end 125 | end 126 | 127 | def handle_call(tlv, _from, state) do 128 | Logger.error("Pair-Setup Received unexpected message: #{inspect(tlv)}, state: #{inspect(state)}") 129 | {:reply, {:error, "Unexpected message for pairing state"}, %{step: 1}} 130 | end 131 | 132 | defp extract_ios_device_exchange(tlv, session_key) do 133 | tlv 134 | |> HAP.TLVParser.parse_tlv() 135 | |> case do 136 | %{ 137 | @kTLVType_Identifier => ios_identifier, 138 | @kTLVType_PublicKey => ios_ltpk, 139 | @kTLVType_Signature => ios_signature 140 | } -> 141 | {:ok, ios_device_x} = 142 | HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info") 143 | 144 | ios_device_info = ios_device_x <> ios_identifier <> ios_ltpk 145 | 146 | case HAP.Crypto.EDDSA.verify(ios_device_info, ios_signature, ios_ltpk) do 147 | {:ok, true} -> {:ok, ios_identifier, ios_ltpk} 148 | _ -> {:error, "Key Verification Error"} 149 | end 150 | 151 | _ -> 152 | {:error, "TLV Parsing Error"} 153 | end 154 | end 155 | 156 | defp build_accessory_device_exchange(accessory_identifier, accessory_ltpk, accessory_ltsk, session_key) do 157 | {:ok, accessory_x} = 158 | HAP.Crypto.HKDF.generate(session_key, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info") 159 | 160 | accessory_info = accessory_x <> accessory_identifier <> accessory_ltpk 161 | {:ok, accessory_signature} = HAP.Crypto.EDDSA.sign(accessory_info, accessory_ltsk) 162 | 163 | sub_tlv = %{ 164 | @kTLVType_Identifier => accessory_identifier, 165 | @kTLVType_PublicKey => accessory_ltpk, 166 | @kTLVType_Signature => accessory_signature 167 | } 168 | 169 | {:ok, HAP.TLVEncoder.to_binary(sub_tlv)} 170 | end 171 | end 172 | --------------------------------------------------------------------------------