(*--------------------------------------------------------------------------- Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. SPDX-License-Identifier: MIT ---------------------------------------------------------------------------*) (** HomeKit Accessory Protocol (HAP). This module implements the HAP protocol for controlling HomeKit accessories: - Discovery via mDNS ({!discover}) - Pair Setup using SRP-6a ({!pair_setup}) - Pair Verify using Curve25519 ({!pair_verify}) - Encrypted sessions using ChaCha20-Poly1305 {2 Protocol Overview} 1. {b Discovery}: Find HomeKit accessories via mDNS (_hap._tcp.local) 2. {b Pair Setup}: One-time pairing using the accessory's PIN code 3. {b Pair Verify}: Establish encrypted session using stored pairing 4. {b Control}: Read/write characteristics over encrypted session {2 References} - {{:https://developer.apple.com/homekit/} Apple HomeKit} - HAP Non-Commercial Specification *) (** {1 Types} *) type pairing = { accessory_id : string; (** Accessory device ID *) accessory_ltpk : string; (** Accessory long-term public key (Ed25519) *) controller_id : string; (** Controller identifier *) controller_ltsk : string; (** Controller long-term secret key (Ed25519) *) controller_ltpk : string; (** Controller long-term public key (Ed25519) *) } (** Controller pairing data. Stored after successful pair setup. *) type accessory_info = { name : string; (** Display name *) device_id : string; (** HAP device ID (MAC address format) *) ip : string; (** IPv4 address *) port : int; (** TCP port *) model : string option; (** Model identifier *) config_num : int; (** Configuration number *) state_num : int; (** State number *) category : int; (** HAP category code *) paired : bool; (** Whether accessory is paired *) } (** Accessory info from mDNS discovery. *) type session (** Encrypted HAP session. *) (** {1 Discovery} *) val discover : sw:Eio.Switch.t -> net:_ Eio.Net.t -> clock:_ Eio.Time.clock -> ?timeout:float -> unit -> accessory_info list (** [discover ~sw ~net ~clock ?timeout ()] finds HomeKit accessories on the local network using mDNS. Default timeout is 3 seconds. *) val accessory_info : sw:Eio.Switch.t -> net:_ Eio.Net.t -> clock:_ Eio.Time.clock -> string -> accessory_info option (** [accessory_info ~sw ~net ~clock ip] returns info for a specific IP. *) val category_name : int -> string (** [category_name code] returns the human-readable name for a HAP category. *) val pp_accessory_info : accessory_info Fmt.t (** Pretty-printer for accessory info. *) (** {1 Pairing} *) val pair_setup : net:_ Eio.Net.t -> sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> ip:string -> port:int -> pin:string -> (pairing, [ `Msg of string ]) result (** [pair_setup ~net ~sw ~clock ~ip ~port ~pin] performs HAP pair setup with an accessory using its PIN code. The PIN is typically in the format "XXX-XX-XXX". This is a one-time operation. Save the resulting {!pairing} for future connections. *) val pair_verify : net:_ Eio.Net.t -> sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> ip:string -> port:int -> pairing:pairing -> (session, [ `Msg of string ]) result (** [pair_verify ~net ~sw ~clock ~ip ~port ~pairing] establishes an encrypted session with a previously paired accessory. *) (** {1 Pairing Storage} *) val save_pairing_by_id : fs:_ Eio.Path.t -> pairing -> string (** [save_pairing_by_id ~fs pairing] saves the pairing to disk. Returns the file path. Pairings are stored in [~/.hap/pairings/]. *) val pairing_by_id : fs:_ Eio.Path.t -> string -> pairing option (** [pairing_by_id ~fs device_id] loads a pairing for the given device. *) val pairing_for_ip : sw:Eio.Switch.t -> net:_ Eio.Net.t -> clock:_ Eio.Time.clock -> fs:_ Eio.Path.t -> string -> pairing option (** [pairing_for_ip ~sw ~net ~clock ~fs ip] discovers the device at [ip] and returns its pairing if one exists. *) (** {1 Session Operations} *) val accessories : net:_ Eio.Net.t -> sw:Eio.Switch.t -> session -> (Jsont.json, [ `Msg of string ]) result (** [accessories ~net ~sw session] returns the accessory database. *) val characteristics : net:_ Eio.Net.t -> sw:Eio.Switch.t -> session -> ids:(int * int) list -> (Jsont.json, [ `Msg of string ]) result (** [characteristics ~net ~sw session ~ids] reads characteristics. Each ID is [(aid, iid)]. *) val put_characteristic : net:_ Eio.Net.t -> sw:Eio.Switch.t -> session -> aid:int -> iid:int -> Jsont.json -> (unit, [ `Msg of string ]) result (** [put_characteristic ~net ~sw session ~aid ~iid value] writes a characteristic value. *) (** {1 High-level Control} *) val turn_on_outlet : net:_ Eio.Net.t -> sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> fs:_ Eio.Path.t -> string -> (unit, [ `Msg of string ]) result (** [turn_on_outlet ~net ~sw ~clock ~fs ip] turns on an outlet at [ip]. Requires existing pairing. *) val turn_off_outlet : net:_ Eio.Net.t -> sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> fs:_ Eio.Path.t -> string -> (unit, [ `Msg of string ]) result (** [turn_off_outlet ~net ~sw ~clock ~fs ip] turns off an outlet at [ip]. *) val toggle_outlet : net:_ Eio.Net.t -> sw:Eio.Switch.t -> clock:_ Eio.Time.clock -> fs:_ Eio.Path.t -> string -> (unit, [ `Msg of string ]) result (** [toggle_outlet ~net ~sw ~clock ~fs ip] toggles an outlet at [ip]. *) (** {1 TLV Encoding} *) (** HAP uses TLV8 encoding for pair setup/verify messages. *) module Tlv : sig type t = (int * string) list val empty : t (** The empty TLV container. *) val add : int -> string -> t -> t (** [add typ value t] adds a TLV entry with the given type and value. *) val get : int -> t -> string option (** [get typ t] returns the value for [typ], or [None] if absent. *) val get_exn : int -> t -> string (** [get_exn typ t] returns the value for [typ]. Raises [Failure] if absent. *) val encode : t -> string (** [encode t] serializes the TLV container to bytes. Values exceeding 255 bytes are automatically chunked. *) val decode : string -> t (** [decode s] deserializes bytes into a TLV container. Consecutive same-type chunks are concatenated. Truncated input is handled gracefully by returning entries parsed so far. *) end (** TLV type codes. *) module Tlv_type : sig val method_ : int (** Pairing method (0x00). *) val identifier : int (** Identifier (0x01). *) val salt : int (** SRP salt (0x02). *) val public_key : int (** SRP or Ed25519 public key (0x03). *) val proof : int (** SRP proof (0x04). *) val encrypted_data : int (** Encrypted data with auth tag (0x05). *) val state : int (** Pair setup/verify state (0x06). *) val error : int (** Error code (0x07). *) val retry_delay : int (** Retry delay in seconds (0x08). *) val certificate : int (** X.509 certificate (0x09). *) val signature : int (** Ed25519 signature (0x0A). *) val permissions : int (** Pairing permissions (0x0B). *) val fragment_data : int (** Fragment data (0x0C). *) val fragment_last : int (** Last fragment (0x0D). *) val separator : int (** Separator between TLV items (0xFF). *) end (** HAP error codes. *) module Hap_error : sig val unknown : int (** Unknown error (0x01). *) val authentication : int (** Authentication failed (0x02). *) val backoff : int (** Too many attempts, client must back off (0x03). *) val max_peers : int (** Maximum number of peers reached (0x04). *) val max_tries : int (** Maximum authentication attempts reached (0x05). *) val unavailable : int (** Accessory is not available for pairing (0x06). *) val busy : int (** Accessory is busy with another pairing (0x07). *) end