From 32ad4f879cec8873f923bef78910d3832889a1d8 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 09:38:19 +0200 Subject: [PATCH 1/7] Rewrite gen_hap_types.py to use macOS HomeKitDaemon metadata Regenerates the characteristics/services catalog from Apple's plain-metadata.config (ships with macOS, updated with every OS release) instead of the Xcode HomeKit Accessory Simulator plist, which lags far behind (163 chars / 48 services vs 242 / 69). Mirrors the approach of HAP-NodeJS's generate-definitions.ts: - plain-metadata.config is the primary source (no Xcode required) - the simulator plist is used as an optional overlay (richer permission strings, ValidValues constraints) when installed - scripts/hap_manual_overrides.json holds hand-maintained additions and overrides for things not yet in Apple's machine-readable metadata (seeded with the HKSV 17.99 definitions, incl. the PR #531 content) - names and required/optional lists are stable across regenerations: UUID-based name resolution plus inheritance rules prevent Apple's description drift and required->optional demotions from breaking existing accessories; ValidValues suppress numeric min/max so invalid values are rejected instead of silently clamped - dry-run by default with an add/remove/change report; --write to apply The regenerated catalogs add 108 characteristics and 30 services (AccessCode, Thread, Siri, WebRTC/HKSV 17.99, power management, etc.). --- pyhap/resources/characteristics.json | 1167 +++++++++++++++++++++++--- pyhap/resources/services.json | 551 +++++++++--- scripts/gen_hap_types.py | 555 ++++++++++-- scripts/hap_manual_overrides.json | 437 ++++++++++ 4 files changed, 2406 insertions(+), 304 deletions(-) create mode 100644 scripts/hap_manual_overrides.json diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index 596e5898..4e9360ab 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -1,4 +1,31 @@ { + "AccessCodeControlPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00000262-0000-1000-8000-0026BB765291" + }, + "AccessCodeSupportedConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000261-0000-1000-8000-0026BB765291" + }, + "AccessControlLevel": { + "Format": "uint16", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "000000E5-0000-1000-8000-0026BB765291", + "maxValue": 2, + "minStep": 1, + "minValue": 0 + }, "AccessoryFlags": { "Format": "uint32", "Permissions": [ @@ -10,6 +37,13 @@ "0": "Requires Additional Setup" } }, + "AccessoryIdentifier": { + "Format": "string", + "Permissions": [ + "pr" + ], + "UUID": "00000057-0000-1000-8000-0026BB765291" + }, "Active": { "Format": "uint8", "Permissions": [ @@ -30,7 +64,8 @@ "pw", "ev" ], - "UUID": "000000E7-0000-1000-8000-0026BB765291" + "UUID": "000000E7-0000-1000-8000-0026BB765291", + "minValue": 0 }, "ActiveTransitionCount": { "Format": "uint8", @@ -40,6 +75,16 @@ ], "UUID": "0000024B-0000-1000-8000-0026BB765291" }, + "ActivityInterval": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000023B-0000-1000-8000-0026BB765291", + "minStep": 1, + "minValue": 0 + }, "AdministratorOnlyAccess": { "Format": "bool", "Permissions": [ @@ -58,7 +103,8 @@ "UUID": "00000064-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "AirParticulateSize": { "Format": "uint8", @@ -99,6 +145,21 @@ "Unknown": 0 } }, + "ApplicationMatchingIdentifier": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "000000A4-0000-1000-8000-0026BB765291" + }, + "AssetUpdateReadiness": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000269-0000-1000-8000-0026BB765291" + }, "AudioFeedback": { "Format": "bool", "Permissions": [ @@ -133,15 +194,136 @@ "minValue": 0, "unit": "percentage" }, + "BufferActivityCommand": { + "Format": "tlv8", + "Permissions": [ + "pw" + ], + "UUID": "00008017-0000-1000-8000-0026BB765291" + }, + "BufferEventCommand": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008014-0000-1000-8000-0026BB765291" + }, + "BufferEventSequenceNumber": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008015-0000-1000-8000-0026BB765291" + }, + "BufferUploadCommand": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008013-0000-1000-8000-0026BB765291" + }, + "ButtonEvent": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000126-0000-1000-8000-0026BB765291" + }, + "CCAEnergyDetectThreshold": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000246-0000-1000-8000-0026BB765291" + }, + "CCASignalDetectThreshold": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000245-0000-1000-8000-0026BB765291" + }, + "CameraCapabilities": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00008011-0000-1000-8000-0026BB765291" + }, + "CameraClientCSR": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008081-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificate": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "tw" + ], + "UUID": "00008082-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificateStatus": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008083-0000-1000-8000-0026BB765291" + }, + "CameraKey": { + "Format": "tlv8", + "Permissions": [ + "pw", + "tw" + ], + "UUID": "00008051-0000-1000-8000-0026BB765291" + }, + "CameraKeyID": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008052-0000-1000-8000-0026BB765291" + }, "CameraOperatingModeIndicator": { "Format": "bool", "Permissions": [ "pr", "pw", - "ev" + "ev", + "tw" ], "UUID": "0000021D-0000-1000-8000-0026BB765291" }, + "CameraRecordingPublishingPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00008016-0000-1000-8000-0026BB765291" + }, + "CameraZones": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00008022-0000-1000-8000-0026BB765291" + }, "CarbonDioxideDetected": { "Format": "uint8", "Permissions": [ @@ -162,7 +344,9 @@ ], "UUID": "00000093-0000-1000-8000-0026BB765291", "maxValue": 100000, - "minValue": 0 + "minStep": 1, + "minValue": 0, + "unit": "ppm" }, "CarbonDioxidePeakLevel": { "Format": "float", @@ -172,7 +356,9 @@ ], "UUID": "00000094-0000-1000-8000-0026BB765291", "maxValue": 100000, - "minValue": 0 + "minStep": 1, + "minValue": 0, + "unit": "ppm" }, "CarbonMonoxideDetected": { "Format": "uint8", @@ -194,7 +380,9 @@ ], "UUID": "00000090-0000-1000-8000-0026BB765291", "maxValue": 100, - "minValue": 0 + "minStep": 1, + "minValue": 0, + "unit": "ppm" }, "CarbonMonoxidePeakLevel": { "Format": "float", @@ -204,7 +392,9 @@ ], "UUID": "00000091-0000-1000-8000-0026BB765291", "maxValue": 100, - "minValue": 0 + "minStep": 1, + "minValue": 0, + "unit": "ppm" }, "ChargingState": { "Format": "uint8", @@ -233,7 +423,7 @@ } }, "ColorTemperature": { - "Format": "uint32", + "Format": "int", "Permissions": [ "pr", "pw", @@ -244,15 +434,31 @@ "minStep": 1, "minValue": 140 }, + "ConfigurationState": { + "Format": "uint16", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000263-0000-1000-8000-0026BB765291" + }, "ConfiguredName": { "Format": "string", "Permissions": [ "pr", - "pw", "ev" ], "UUID": "000000E3-0000-1000-8000-0026BB765291" }, + "ConnectionHealthMonitor": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000273-0000-1000-8000-0026BB765291" + }, "ContactSensorState": { "Format": "uint8", "Permissions": [ @@ -265,6 +471,14 @@ "ContactNotDetected": 1 } }, + "ContributingSensors": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008086-0000-1000-8000-0026BB765291" + }, "CoolingThresholdTemperature": { "Format": "float", "Permissions": [ @@ -391,8 +605,8 @@ ], "UUID": "000000E0-0000-1000-8000-0026BB765291", "ValidValues": { - "Play": 0, "Pause": 1, + "Play": 0, "Stop": 2, "Unknown": 3 } @@ -441,9 +655,9 @@ "ev" ], "UUID": "00000011-0000-1000-8000-0026BB765291", - "maxValue": 1000, + "maxValue": 100, "minStep": 0.1, - "minValue": -273.1, + "minValue": 0, "unit": "celsius" }, "CurrentTiltAngle": { @@ -458,6 +672,13 @@ "minValue": -90, "unit": "arcdegrees" }, + "CurrentTransport": { + "Format": "bool", + "Permissions": [ + "pr" + ], + "UUID": "0000022B-0000-1000-8000-0026BB765291" + }, "CurrentVerticalTiltAngle": { "Format": "int", "Permissions": [ @@ -470,7 +691,7 @@ "minValue": -90, "unit": "arcdegrees" }, - "CurrentVisibilityState":{ + "CurrentVisibilityState": { "Format": "uint8", "Permissions": [ "pr", @@ -478,12 +699,28 @@ ], "UUID": "00000135-0000-1000-8000-0026BB765291", "ValidValues": { - "Shown": 0, "Hidden": 1, + "Shown": 0, "State2": 2, "State3": 3 } }, + "DataStreamHAPTransport": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00000138-0000-1000-8000-0026BB765291" + }, + "DataStreamHAPTransportInterrupt": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000139-0000-1000-8000-0026BB765291" + }, "DigitalZoom": { "Format": "float", "Permissions": [ @@ -491,23 +728,31 @@ "pw", "ev" ], - "UUID": "0000011D-0000-1000-8000-0026BB765291" + "UUID": "0000011D-0000-1000-8000-0026BB765291", + "minStep": 0.1 }, "DisplayOrder": { "Format": "tlv8", "Permissions": [ "pr", - "pw", "ev" ], "UUID": "00000136-0000-1000-8000-0026BB765291" }, - "EventSnapshotsActive": { + "EventRetransmissionMaximum": { "Format": "uint8", + "Permissions": [ + "pr" + ], + "UUID": "0000023D-0000-1000-8000-0026BB765291" + }, + "EventSnapshotsActive": { + "Format": "bool", "Permissions": [ "pr", "pw", - "ev" + "ev", + "tw" ], "UUID": "00000223-0000-1000-8000-0026BB765291", "ValidValues": { @@ -515,6 +760,13 @@ "Enable": 1 } }, + "EventTransmissionCounters": { + "Format": "uint32", + "Permissions": [ + "pr" + ], + "UUID": "0000023E-0000-1000-8000-0026BB765291" + }, "FilterChangeIndication": { "Format": "uint8", "Permissions": [ @@ -535,6 +787,7 @@ ], "UUID": "000000AB-0000-1000-8000-0026BB765291", "maxValue": 100, + "minStep": 1, "minValue": 0, "stepValue": 1 }, @@ -545,6 +798,36 @@ ], "UUID": "00000052-0000-1000-8000-0026BB765291" }, + "FirmwareUpdateProtocolList": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "0000026F-0000-1000-8000-0026BB765291" + }, + "FirmwareUpdateReadiness": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000234-0000-1000-8000-0026BB765291" + }, + "FirmwareUpdateStatus": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000235-0000-1000-8000-0026BB765291" + }, + "HardwareFinish": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "0000026C-0000-1000-8000-0026BB765291" + }, "HardwareRevision": { "Format": "string", "Permissions": [ @@ -552,6 +835,14 @@ ], "UUID": "00000053-0000-1000-8000-0026BB765291" }, + "HeartBeat": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000024A-0000-1000-8000-0026BB765291" + }, "HeatingThresholdTemperature": { "Format": "float", "Permissions": [ @@ -573,7 +864,7 @@ "UUID": "0000006F-0000-1000-8000-0026BB765291" }, "HomeKitCameraActive": { - "Format": "uint8", + "Format": "bool", "Permissions": [ "pr", "pw", @@ -625,40 +916,31 @@ "UUID": "0000011F-0000-1000-8000-0026BB765291" }, "ImageRotation": { - "Format": "float", + "Format": "int", "Permissions": [ "pr", "pw", "ev" ], "UUID": "0000011E-0000-1000-8000-0026BB765291", - "maxValue": 270, - "minStep": 90, + "maxValue": 360, + "minStep": 1, "minValue": 0, "unit": "arcdegrees" }, - "InputSourceType":{ + "InUse": { "Format": "uint8", "Permissions": [ "pr", "ev" ], - "UUID": "000000DB-0000-1000-8000-0026BB765291", + "UUID": "000000D2-0000-1000-8000-0026BB765291", "ValidValues": { - "Other": 0, - "HomeScreen": 1, - "Tuner": 2, - "HDMI": 3, - "CompositeVideo": 4, - "SVideo": 5, - "ComponentVideo": 6, - "DVI": 7, - "AirPlay": 8, - "USB": 9, - "Application": 10 + "Inuse": 1, + "Notinuse": 0 } }, - "InputDeviceType":{ + "InputDeviceType": { "Format": "uint8", "Permissions": [ "pr", @@ -666,31 +948,39 @@ ], "UUID": "000000DC-0000-1000-8000-0026BB765291", "ValidValues": { + "AudioSystem": 5, "Other": 0, - "TV": 1, - "Recording": 2, - "Tuner": 3, "Playback": 4, - "AudioSystem": 5 + "Recording": 2, + "TV": 1, + "Tuner": 3 } }, - "InUse": { + "InputSourceType": { "Format": "uint8", "Permissions": [ "pr", "ev" ], - "UUID": "000000D2-0000-1000-8000-0026BB765291", + "UUID": "000000DB-0000-1000-8000-0026BB765291", "ValidValues": { - "Inuse": 1, - "Notinuse": 0 + "AirPlay": 8, + "Application": 10, + "ComponentVideo": 6, + "CompositeVideo": 4, + "DVI": 7, + "HDMI": 3, + "HomeScreen": 1, + "Other": 0, + "SVideo": 5, + "Tuner": 2, + "USB": 9 } }, "IsConfigured": { "Format": "uint8", "Permissions": [ "pr", - "pw", "ev" ], "UUID": "000000D6-0000-1000-8000-0026BB765291", @@ -795,6 +1085,32 @@ ], "UUID": "0000001F-0000-1000-8000-0026BB765291" }, + "MACRetransmissionMaximum": { + "Format": "uint8", + "Permissions": [ + "pr" + ], + "UUID": "00000247-0000-1000-8000-0026BB765291" + }, + "MACTransmissionCounters": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "00000248-0000-1000-8000-0026BB765291" + }, + "ManagedNetworkEnable": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00000215-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, "ManuallyDisabled": { "Format": "bool", "Permissions": [ @@ -808,14 +1124,59 @@ "Permissions": [ "pr" ], - "UUID": "00000020-0000-1000-8000-0026BB765291" + "UUID": "00000020-0000-1000-8000-0026BB765291", + "maxLen": 64 + }, + "MatterFirmwareRevisionNumber": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000026D-0000-1000-8000-0026BB765291" + }, + "MatterFirmwareUpdateStatus": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000026E-0000-1000-8000-0026BB765291" + }, + "MaximumTransmitPower": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000243-0000-1000-8000-0026BB765291" + }, + "MediaAccessControlLevel": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "000000EE-0000-1000-8000-0026BB765291", + "maxValue": 2, + "minStep": 1, + "minValue": 0 + }, + "MetricsBufferFullState": { + "Format": "bool", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000272-0000-1000-8000-0026BB765291" }, "Model": { "Format": "string", "Permissions": [ "pr" ], - "UUID": "00000021-0000-1000-8000-0026BB765291" + "UUID": "00000021-0000-1000-8000-0026BB765291", + "maxLen": 64 }, "MotionDetected": { "Format": "bool", @@ -825,6 +1186,29 @@ ], "UUID": "00000022-0000-1000-8000-0026BB765291" }, + "MotionEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008087-0000-1000-8000-0026BB765291" + }, + "MultifunctionButton": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000026B-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, "Mute": { "Format": "bool", "Permissions": [ @@ -834,19 +1218,67 @@ ], "UUID": "0000011A-0000-1000-8000-0026BB765291" }, + "NFCAccessControlPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00000264-0000-1000-8000-0026BB765291" + }, + "NFCAccessSupportedConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000265-0000-1000-8000-0026BB765291" + }, "Name": { "Format": "string", "Permissions": [ "pr" ], - "UUID": "00000023-0000-1000-8000-0026BB765291" + "UUID": "00000023-0000-1000-8000-0026BB765291", + "maxLen": 64 + }, + "NetworkAccessViolationControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw", + "wr" + ], + "UUID": "0000021F-0000-1000-8000-0026BB765291" + }, + "NetworkClientControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw", + "wr" + ], + "UUID": "0000020C-0000-1000-8000-0026BB765291" + }, + "NetworkClientStatusControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "0000020D-0000-1000-8000-0026BB765291" }, "NightVision": { "Format": "bool", "Permissions": [ "pr", "pw", - "ev" + "ev", + "tw" ], "UUID": "0000011B-0000-1000-8000-0026BB765291" }, @@ -859,7 +1291,8 @@ "UUID": "000000C4-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "ObstructionDetected": { "Format": "bool", @@ -890,6 +1323,14 @@ ], "UUID": "00000025-0000-1000-8000-0026BB765291" }, + "OperatingStateResponse": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000232-0000-1000-8000-0026BB765291" + }, "OpticalZoom": { "Format": "float", "Permissions": [ @@ -897,7 +1338,8 @@ "pw", "ev" ], - "UUID": "0000011C-0000-1000-8000-0026BB765291" + "UUID": "0000011C-0000-1000-8000-0026BB765291", + "minStep": 0.1 }, "OutletInUse": { "Format": "bool", @@ -916,7 +1358,8 @@ "UUID": "000000C3-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "PM10Density": { "Format": "float", @@ -927,7 +1370,8 @@ "UUID": "000000C7-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "PM2.5Density": { "Format": "float", @@ -938,7 +1382,8 @@ "UUID": "000000C6-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "PairSetup": { "Format": "tlv8", @@ -959,7 +1404,8 @@ "PairingFeatures": { "Format": "uint8", "Permissions": [ - "pr" + "pr", + "pw" ], "UUID": "0000004F-0000-1000-8000-0026BB765291" }, @@ -971,20 +1417,30 @@ ], "UUID": "00000050-0000-1000-8000-0026BB765291" }, - "PeriodicSnapshotsActive": { - "Format": "uint8", + "PasswordSetting": { + "Format": "tlv8", "Permissions": [ "pr", "pw", "ev" ], + "UUID": "000000E4-0000-1000-8000-0026BB765291" + }, + "PeriodicSnapshotsActive": { + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], "UUID": "00000225-0000-1000-8000-0026BB765291", "ValidValues": { "Disable": 0, "Enable": 1 } }, - "PictureMode":{ + "PictureMode": { "Format": "uint8", "Permissions": [ "pr", @@ -992,19 +1448,24 @@ "ev" ], "UUID": "000000E2-0000-1000-8000-0026BB765291", - "maxValue": 13, - "minValue": 0, "ValidValues": { - "Other": 0, - "Standard": 1, "Calibrated": 2, "CalibratedDark": 3, - "Vivid": 4, - "Game": 5, "Computer": 6, - "Custom": 7 + "Custom": 7, + "Game": 5, + "Other": 0, + "Standard": 1, + "Vivid": 4 } }, + "Ping": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "0000023C-0000-1000-8000-0026BB765291" + }, "PositionState": { "Format": "uint8", "Permissions": [ @@ -1018,17 +1479,33 @@ "Stopped": 2 } }, - "PowerModeSelection":{ + "PowerModeSelection": { "Format": "uint8", "Permissions": [ "pw" ], "UUID": "000000DF-0000-1000-8000-0026BB765291", "ValidValues": { - "Show": 0, - "Hide": 1 + "Hide": 1, + "Show": 0 } }, + "PreferredWiFiList": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "0000022E-0000-1000-8000-0026BB765291" + }, + "ProductData": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "00000220-0000-1000-8000-0026BB765291" + }, "ProgramMode": { "Format": "uint8", "Permissions": [ @@ -1055,6 +1532,41 @@ "SinglePress": 0 } }, + "ProgrammableSwitchOutputState": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000074-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minStep": 1, + "minValue": 0 + }, + "RTPStreamingControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008045-0000-1000-8000-0026BB765291" + }, + "ReceivedSignalStrengthIndication": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "0000023F-0000-1000-8000-0026BB765291" + }, + "ReceiverSensitivity": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000244-0000-1000-8000-0026BB765291" + }, "RecordingAudioActive": { "Format": "uint8", "Permissions": [ @@ -1104,30 +1616,29 @@ "UUID": "000000D4-0000-1000-8000-0026BB765291", "maxValue": 3600, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "seconds" }, - "RemoteKey":{ + "RemoteKey": { "Format": "uint8", "Permissions": [ "pw" ], "UUID": "000000E1-0000-1000-8000-0026BB765291", - "maxValue": 16, - "minValue": 0, "ValidValues": { - "Rewind": 0, - "FastForward": 1, - "NextTrack": 2, - "PreviousTrack": 3, - "ArrowUp": 4, "ArrowDown": 5, "ArrowLeft": 6, "ArrowRight": 7, - "Select": 8, + "ArrowUp": 4, "Back": 9, "Exit": 10, + "FastForward": 1, + "Information": 15, + "NextTrack": 2, "PlayPause": 11, - "Information": 15 + "PreviousTrack": 3, + "Rewind": 0, + "Select": 8 } }, "ResetFilterIndication": { @@ -1166,6 +1677,16 @@ "minValue": 0, "unit": "percentage" }, + "RouterStatus": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000020E-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, "Saturation": { "Format": "float", "Permissions": [ @@ -1220,6 +1741,14 @@ "StayArm": 0 } }, + "SelectedAudioStreamConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00000128-0000-1000-8000-0026BB765291" + }, "SelectedCameraRecordingConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1229,6 +1758,14 @@ ], "UUID": "00000209-0000-1000-8000-0026BB765291" }, + "SelectedDiagnosticsModes": { + "Format": "uint32", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "0000024D-0000-1000-8000-0026BB765291" + }, "SelectedRTPStreamConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1237,13 +1774,40 @@ ], "UUID": "00000117-0000-1000-8000-0026BB765291" }, - "SerialNumber": { - "Format": "string", - "MaximumLength": 64, + "SelectedSessionKeepaliveConfigurationList": { + "Format": "tlv8", "Permissions": [ - "pr" + "pr", + "pw", + "ev" + ], + "UUID": "00000275-0000-1000-8000-0026BB765291" + }, + "SelectedSleepConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev", + "wr" + ], + "UUID": "00000252-0000-1000-8000-0026BB765291" + }, + "SensorUUID": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "0000805B-0000-1000-8000-0026BB765291" + }, + "SerialNumber": { + "Format": "string", + "MaximumLength": 64, + "Permissions": [ + "pr" ], - "UUID": "00000030-0000-1000-8000-0026BB765291" + "UUID": "00000030-0000-1000-8000-0026BB765291", + "maxLen": 64 }, "ServiceLabelIndex": { "Format": "uint8", @@ -1266,6 +1830,14 @@ "Dots": 0 } }, + "ServiceSignature": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "000000A5-0000-1000-8000-0026BB765291" + }, "SetDuration": { "Format": "uint32", "Permissions": [ @@ -1276,14 +1848,14 @@ "UUID": "000000D3-0000-1000-8000-0026BB765291", "maxValue": 3600, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "seconds" }, "SetupDataStreamTransport": { "Format": "tlv8", "Permissions": [ "pr", - "pw", - "wr" + "pw" ], "UUID": "00000131-0000-1000-8000-0026BB765291" }, @@ -1295,6 +1867,89 @@ ], "UUID": "00000118-0000-1000-8000-0026BB765291" }, + "SetupTransferTransport": { + "Format": "tlv8", + "Permissions": [ + "pw", + "wr" + ], + "UUID": "00000201-0000-1000-8000-0026BB765291" + }, + "SignalToNoiseRatio": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000241-0000-1000-8000-0026BB765291" + }, + "SiriEnable": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000255-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, + "SiriEndpointSessionStatus": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000254-0000-1000-8000-0026BB765291" + }, + "SiriEngineVersion": { + "Format": "string", + "Permissions": [ + "pr" + ], + "UUID": "0000025A-0000-1000-8000-0026BB765291" + }, + "SiriInputType": { + "Format": "uint8", + "Permissions": [ + "pr" + ], + "UUID": "00000132-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, + "SiriLightOnUse": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000258-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, + "SiriListening": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000256-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, + "SiriTouchToUse": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000257-0000-1000-8000-0026BB765291", + "maxValue": 1, + "minValue": 0 + }, "SlatType": { "Format": "uint8", "Permissions": [ @@ -1314,10 +1969,20 @@ ], "UUID": "000000E8-0000-1000-8000-0026BB765291", "ValidValues": { - "NotDiscoverable": 0, - "AlwaysDiscoverable": 1 + "AlwaysDiscoverable": 1, + "NotDiscoverable": 0 } }, + "SleepInterval": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000023A-0000-1000-8000-0026BB765291", + "minStep": 1, + "minValue": 0 + }, "SmokeDetected": { "Format": "uint8", "Permissions": [ @@ -1330,6 +1995,21 @@ "SmokeNotDetected": 0 } }, + "SoftwareRevision": { + "Format": "string", + "Permissions": [ + "pr" + ], + "UUID": "00000054-0000-1000-8000-0026BB765291" + }, + "StagedFirmwareVersion": { + "Format": "string", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000249-0000-1000-8000-0026BB765291" + }, "StatusActive": { "Format": "bool", "Permissions": [ @@ -1386,6 +2066,19 @@ "Tampered": 1 } }, + "StreamingEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008041-0000-1000-8000-0026BB765291" + }, "StreamingStatus": { "Format": "tlv8", "Permissions": [ @@ -1403,7 +2096,15 @@ "UUID": "000000C5-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" + }, + "SupportedAssetTypes": { + "Format": "uint32", + "Permissions": [ + "pr" + ], + "UUID": "00000268-0000-1000-8000-0026BB765291" }, "SupportedAudioRecordingConfiguration": { "Format": "tlv8", @@ -1420,6 +2121,14 @@ ], "UUID": "00000115-0000-1000-8000-0026BB765291" }, + "SupportedAudioStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008044-0000-1000-8000-0026BB765291" + }, "SupportedCameraRecordingConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1435,6 +2144,35 @@ ], "UUID": "00000130-0000-1000-8000-0026BB765291" }, + "SupportedDiagnosticsModes": { + "Format": "uint32", + "Permissions": [ + "pr" + ], + "UUID": "0000024C-0000-1000-8000-0026BB765291" + }, + "SupportedDiagnosticsSnapshot": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000238-0000-1000-8000-0026BB765291" + }, + "SupportedFirmwareUpdateConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000233-0000-1000-8000-0026BB765291" + }, + "SupportedMetrics": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00000271-0000-1000-8000-0026BB765291" + }, "SupportedRTPConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1442,6 +2180,48 @@ ], "UUID": "00000116-0000-1000-8000-0026BB765291" }, + "SupportedRouterConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000210-0000-1000-8000-0026BB765291" + }, + "SupportedSessionKeepaliveConfigurationList": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000274-0000-1000-8000-0026BB765291" + }, + "SupportedSleepConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000251-0000-1000-8000-0026BB765291" + }, + "SupportedTargetConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000123-0000-1000-8000-0026BB765291" + }, + "SupportedTransferTransportConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000202-0000-1000-8000-0026BB765291" + }, + "SupportedTransitionConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000144-0000-1000-8000-0026BB765291" + }, "SupportedVideoRecordingConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1457,12 +2237,13 @@ ], "UUID": "00000114-0000-1000-8000-0026BB765291" }, - "SupportedTransitionConfiguration": { + "SupportedVideoStreamTiers": { "Format": "tlv8", "Permissions": [ - "pr" + "pr", + "ev" ], - "UUID": "00000144-0000-1000-8000-0026BB765291" + "UUID": "00008043-0000-1000-8000-0026BB765291" }, "SwingMode": { "Format": "uint8", @@ -1586,6 +2367,15 @@ "HumidifierorDehumidifier": 0 } }, + "TargetListConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000124-0000-1000-8000-0026BB765291" + }, "TargetMediaState": { "Format": "uint8", "Permissions": [ @@ -1595,8 +2385,8 @@ ], "UUID": "00000137-0000-1000-8000-0026BB765291", "ValidValues": { - "Play": 0, "Pause": 1, + "Play": 0, "Stop": 2 } }, @@ -1678,7 +2468,7 @@ "minValue": -90, "unit": "arcdegrees" }, - "TargetVisibilityState":{ + "TargetVisibilityState": { "Format": "uint8", "Permissions": [ "pr", @@ -1687,8 +2477,8 @@ ], "UUID": "00000134-0000-1000-8000-0026BB765291", "ValidValues": { - "Shown": 0, - "Hidden": 1 + "Hidden": 1, + "Shown": 0 } }, "TemperatureDisplayUnits": { @@ -1708,20 +2498,60 @@ "Format": "bool", "Permissions": [ "pr", - "pw", "ev" ], "UUID": "0000021C-0000-1000-8000-0026BB765291" }, + "ThreadControlPoint": { + "Format": "tlv8", + "Permissions": [ + "pw" + ], + "UUID": "00000704-0000-1000-8000-0026BB765291" + }, + "ThreadNodeCapabilities": { + "Format": "uint16", + "Permissions": [ + "pr" + ], + "UUID": "00000702-0000-1000-8000-0026BB765291", + "maxValue": 31, + "minStep": 1, + "minValue": 0 + }, + "ThreadOpenThreadVersion": { + "Format": "string", + "Permissions": [ + "pr" + ], + "UUID": "00000706-0000-1000-8000-0026BB765291" + }, + "ThreadStatus": { + "Format": "uint16", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000703-0000-1000-8000-0026BB765291", + "maxValue": 6, + "minStep": 1, + "minValue": 0 + }, "TransitionControl": { "Format": "tlv8", "Permissions": [ "pr", - "pw", - "wr" + "pw" ], "UUID": "00000143-0000-1000-8000-0026BB765291" }, + "TransmitPower": { + "Format": "int", + "Permissions": [ + "pr" + ], + "UUID": "00000242-0000-1000-8000-0026BB765291" + }, "VOCDensity": { "Format": "float", "Permissions": [ @@ -1731,7 +2561,8 @@ "UUID": "000000C8-0000-1000-8000-0026BB765291", "maxValue": 1000, "minStep": 1, - "minValue": 0 + "minValue": 0, + "unit": "micrograms/m^3" }, "ValveType": { "Format": "uint8", @@ -1753,7 +2584,17 @@ "Permissions": [ "pr" ], - "UUID": "00000037-0000-1000-8000-0026BB765291" + "UUID": "00000037-0000-1000-8000-0026BB765291", + "maxLen": 64 + }, + "VideoAnalysisActive": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "00000229-0000-1000-8000-0026BB765291" }, "Volume": { "Format": "uint8", @@ -1776,10 +2617,10 @@ ], "UUID": "000000E9-0000-1000-8000-0026BB765291", "ValidValues": { + "Absolute": 3, "None": 0, "Relative": 1, - "RelativeWithCurrent": 2, - "Absolute": 3 + "RelativeWithCurrent": 2 } }, "VolumeSelector": { @@ -1789,10 +2630,33 @@ ], "UUID": "000000EA-0000-1000-8000-0026BB765291", "ValidValues": { - "Increment": 0, - "Decrement": 1 + "Decrement": 1, + "Increment": 0 } }, + "WANConfigurationList": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000211-0000-1000-8000-0026BB765291" + }, + "WANStatusList": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000212-0000-1000-8000-0026BB765291" + }, + "WakeConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000222-0000-1000-8000-0026BB765291" + }, "WaterLevel": { "Format": "float", "Permissions": [ @@ -1801,38 +2665,107 @@ ], "UUID": "000000B5-0000-1000-8000-0026BB765291", "maxValue": 100, + "minStep": 1, "minValue": 0, "unit": "percentage" }, - "HardwareFinish": { + "WebRTCNumberOfActiveSessions": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008057-0000-1000-8000-0026BB765291", + "maxValue": 255, + "minValue": 0 + }, + "WebRTCProvideAnswer": { "Format": "tlv8", "Permissions": [ - "pr" + "pr", + "pw", + "wr" ], - "UUID": "0000026C-0000-1000-8000-0026BB765291" + "UUID": "00008054-0000-1000-8000-0026BB765291" }, - "ConfigurationState": { - "Format": "uint16", + "WebRTCReoffer": { + "Format": "tlv8", "Permissions": [ "pr", - "ev" + "pw", + "wr" ], - "UUID": "00000263-0000-1000-8000-0026BB765291" + "UUID": "00008058-0000-1000-8000-0026BB765291" }, - "NFCAccessControlPoint": { + "WebRTCSolicitOffer": { "Format": "tlv8", "Permissions": [ "pr", "pw", "wr" ], - "UUID": "00000264-0000-1000-8000-0026BB765291" + "UUID": "00008053-0000-1000-8000-0026BB765291" }, - "NFCAccessSupportedConfiguration": { + "WebRTCStreamingControl": { "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008056-0000-1000-8000-0026BB765291" + }, + "WebRTCSupportedAudioStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000805A-0000-1000-8000-0026BB765291" + }, + "WebRTCSupportedVideoStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008059-0000-1000-8000-0026BB765291" + }, + "WebRTCUpdateSession": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "0000805C-0000-1000-8000-0026BB765291" + }, + "WiFiCapabilities": { + "Format": "uint32", "Permissions": [ "pr" ], - "UUID": "00000265-0000-1000-8000-0026BB765291" + "UUID": "0000022C-0000-1000-8000-0026BB765291" + }, + "WiFiConfigurationControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw", + "wr" + ], + "UUID": "0000022D-0000-1000-8000-0026BB765291" + }, + "WiFiSatelliteStatus": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000021E-0000-1000-8000-0026BB765291", + "maxValue": 2, + "minValue": 0 } } diff --git a/pyhap/resources/services.json b/pyhap/resources/services.json index 98c95aa4..46420901 100644 --- a/pyhap/resources/services.json +++ b/pyhap/resources/services.json @@ -1,9 +1,33 @@ { + "AccessCode": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "AccessCodeControlPoint", + "AccessCodeSupportedConfiguration", + "ConfigurationState" + ], + "UUID": "00000260-0000-1000-8000-0026BB765291" + }, + "AccessControl": { + "OptionalCharacteristics": [ + "MediaAccessControlLevel", + "PasswordSetting" + ], + "RequiredCharacteristics": [ + "AccessControlLevel" + ], + "UUID": "000000DA-0000-1000-8000-0026BB765291" + }, "AccessoryInformation": { "OptionalCharacteristics": [ - "HardwareRevision", "AccessoryFlags", - "HardwareFinish" + "ApplicationMatchingIdentifier", + "ConfiguredName", + "MatterFirmwareRevisionNumber", + "HardwareFinish", + "HardwareRevision", + "ProductData", + "SoftwareRevision" ], "RequiredCharacteristics": [ "Identify", @@ -15,12 +39,35 @@ ], "UUID": "0000003E-0000-1000-8000-0026BB765291" }, + "AccessoryMetrics": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "Active", + "MetricsBufferFullState", + "SupportedMetrics" + ], + "UUID": "00000270-0000-1000-8000-0026BB765291" + }, + "AccessoryRuntimeInformation": { + "OptionalCharacteristics": [ + "ActivityInterval", + "ConnectionHealthMonitor", + "HeartBeat", + "SelectedSessionKeepaliveConfigurationList", + "SleepInterval", + "SupportedSessionKeepaliveConfigurationList" + ], + "RequiredCharacteristics": [ + "Ping" + ], + "UUID": "00000239-0000-1000-8000-0026BB765291" + }, "AirPurifier": { "OptionalCharacteristics": [ "LockPhysicalControls", "Name", - "SwingMode", - "RotationSpeed" + "RotationSpeed", + "SwingMode" ], "RequiredCharacteristics": [ "Active", @@ -31,17 +78,17 @@ }, "AirQualitySensor": { "OptionalCharacteristics": [ - "StatusActive", - "StatusFault", - "StatusTampered", - "StatusLowBattery", - "Name", - "OzoneDensity", "NitrogenDioxideDensity", - "SulphurDioxideDensity", - "PM2.5Density", + "OzoneDensity", "PM10Density", + "PM2.5Density", + "SulphurDioxideDensity", "VOCDensity", + "Name", + "StatusActive", + "StatusFault", + "StatusLowBattery", + "StatusTampered", "CarbonMonoxideLevel", "CarbonDioxideLevel" ], @@ -50,17 +97,116 @@ ], "UUID": "0000008D-0000-1000-8000-0026BB765291" }, + "AssetUpdate": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "AssetUpdateReadiness", + "SupportedAssetTypes" + ], + "UUID": "00000267-0000-1000-8000-0026BB765291" + }, + "Assistant": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "Active", + "Identifier", + "Name" + ], + "UUID": "0000026A-0000-1000-8000-0026BB765291" + }, + "AudioStreamManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "SupportedAudioStreamConfiguration", + "SelectedAudioStreamConfiguration" + ], + "UUID": "00000127-0000-1000-8000-0026BB765291" + }, "BatteryService": { "OptionalCharacteristics": [ "Name" ], "RequiredCharacteristics": [ + "StatusLowBattery", "BatteryLevel", - "ChargingState", - "StatusLowBattery" + "ChargingState" ], "UUID": "00000096-0000-1000-8000-0026BB765291" }, + "CameraBufferManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "BufferUploadCommand", + "BufferActivityCommand", + "BufferEventCommand", + "BufferEventSequenceNumber", + "CameraRecordingPublishingPoint" + ], + "UUID": "00008000-0000-1000-8000-0026BB765291" + }, + "CameraCapabilities": { + "OptionalCharacteristics": [ + "ManuallyDisabled" + ], + "RequiredCharacteristics": [ + "Version", + "CameraCapabilities" + ], + "UUID": "00008010-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificateManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "CameraClientCSR", + "CameraClientCertificate", + "CameraClientCertificateStatus" + ], + "UUID": "00008080-0000-1000-8000-0026BB765291" + }, + "CameraGlobalOperatingMode": { + "OptionalCharacteristics": [ + "ManuallyDisabled", + "NightVision", + "ThirdPartyCameraActive" + ], + "RequiredCharacteristics": [ + "HomeKitCameraActive", + "StreamingEnabled", + "CameraOperatingModeIndicator" + ], + "UUID": "00008032-0000-1000-8000-0026BB765291" + }, + "CameraKeyManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "CameraKey", + "CameraKeyID" + ], + "UUID": "00008050-0000-1000-8000-0026BB765291" + }, + "CameraMotionZones": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "Version", + "Active", + "CameraZones" + ], + "UUID": "00008021-0000-1000-8000-0026BB765291" + }, + "CameraMultiTierRTPStreamManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "StreamingEnabled", + "StatusActive", + "SupportedVideoStreamTiers", + "SupportedAudioStreamTiers", + "SupportedRTPConfiguration", + "SetupEndpoints", + "RTPStreamingControl", + "SensorUUID" + ], + "UUID": "00008031-0000-1000-8000-0026BB765291" + }, "CameraOperatingMode": { "OptionalCharacteristics": [ "CameraOperatingModeIndicator", @@ -81,12 +227,12 @@ "Name" ], "RequiredCharacteristics": [ - "SupportedVideoStreamConfiguration", - "SupportedAudioStreamConfiguration", - "SupportedRTPConfiguration", "SelectedRTPStreamConfiguration", + "SetupEndpoints", "StreamingStatus", - "SetupEndpoints" + "SupportedAudioStreamConfiguration", + "SupportedRTPConfiguration", + "SupportedVideoStreamConfiguration" ], "UUID": "00000110-0000-1000-8000-0026BB765291" }, @@ -103,15 +249,31 @@ ], "UUID": "00000204-0000-1000-8000-0026BB765291" }, + "CameraWebRTCStreamManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "WebRTCSolicitOffer", + "WebRTCProvideAnswer", + "WebRTCStreamingControl", + "WebRTCNumberOfActiveSessions", + "WebRTCReoffer", + "WebRTCUpdateSession", + "WebRTCSupportedVideoStreamTiers", + "WebRTCSupportedAudioStreamTiers", + "StreamingEnabled", + "SensorUUID" + ], + "UUID": "00008033-0000-1000-8000-0026BB765291" + }, "CarbonDioxideSensor": { "OptionalCharacteristics": [ + "CarbonDioxideLevel", + "CarbonDioxidePeakLevel", + "Name", "StatusActive", "StatusFault", "StatusLowBattery", - "StatusTampered", - "CarbonDioxideLevel", - "CarbonDioxidePeakLevel", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "CarbonDioxideDetected" @@ -120,13 +282,13 @@ }, "CarbonMonoxideSensor": { "OptionalCharacteristics": [ + "CarbonMonoxideLevel", + "CarbonMonoxidePeakLevel", + "Name", "StatusActive", "StatusFault", "StatusLowBattery", - "StatusTampered", - "CarbonMonoxideLevel", - "CarbonMonoxidePeakLevel", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "CarbonMonoxideDetected" @@ -135,11 +297,11 @@ }, "ContactSensor": { "OptionalCharacteristics": [ + "Name", "StatusActive", "StatusFault", - "StatusTampered", "StatusLowBattery", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "ContactSensorState" @@ -155,11 +317,21 @@ ], "UUID": "00000129-0000-1000-8000-0026BB765291" }, + "Diagnostics": { + "OptionalCharacteristics": [ + "SelectedDiagnosticsModes", + "SupportedDiagnosticsModes" + ], + "RequiredCharacteristics": [ + "SupportedDiagnosticsSnapshot" + ], + "UUID": "00000237-0000-1000-8000-0026BB765291" + }, "Door": { "OptionalCharacteristics": [ - "HoldPosition", + "Name", "ObstructionDetected", - "Name" + "HoldPosition" ], "RequiredCharacteristics": [ "CurrentPosition", @@ -171,8 +343,10 @@ "Doorbell": { "OptionalCharacteristics": [ "Brightness", - "Volume", - "Name" + "Mute", + "Name", + "OperatingStateResponse", + "Volume" ], "RequiredCharacteristics": [ "ProgrammableSwitchEvent" @@ -181,9 +355,9 @@ }, "Fan": { "OptionalCharacteristics": [ + "Name", "RotationDirection", - "RotationSpeed", - "Name" + "RotationSpeed" ], "RequiredCharacteristics": [ "On" @@ -226,6 +400,19 @@ ], "UUID": "000000BA-0000-1000-8000-0026BB765291" }, + "FirmwareUpdate": { + "OptionalCharacteristics": [ + "FirmwareUpdateProtocolList", + "MatterFirmwareUpdateStatus", + "StagedFirmwareVersion", + "SupportedFirmwareUpdateConfiguration" + ], + "RequiredCharacteristics": [ + "FirmwareUpdateReadiness", + "FirmwareUpdateStatus" + ], + "UUID": "00000236-0000-1000-8000-0026BB765291" + }, "GarageDoorOpener": { "OptionalCharacteristics": [ "LockCurrentState", @@ -243,11 +430,11 @@ "OptionalCharacteristics": [ "LockPhysicalControls", "Name", + "RotationSpeed", "SwingMode", "CoolingThresholdTemperature", "HeatingThresholdTemperature", - "TemperatureDisplayUnits", - "RotationSpeed" + "TemperatureDisplayUnits" ], "RequiredCharacteristics": [ "Active", @@ -261,27 +448,27 @@ "OptionalCharacteristics": [ "LockPhysicalControls", "Name", - "SwingMode", - "WaterLevel", "RelativeHumidityDehumidifierThreshold", "RelativeHumidityHumidifierThreshold", - "RotationSpeed" + "RotationSpeed", + "SwingMode", + "WaterLevel" ], "RequiredCharacteristics": [ - "CurrentRelativeHumidity", + "Active", "CurrentHumidifierDehumidifierState", "TargetHumidifierDehumidifierState", - "Active" + "CurrentRelativeHumidity" ], "UUID": "000000BD-0000-1000-8000-0026BB765291" }, "HumiditySensor": { "OptionalCharacteristics": [ + "Name", "StatusActive", "StatusFault", - "StatusTampered", "StatusLowBattery", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "CurrentRelativeHumidity" @@ -292,21 +479,21 @@ "OptionalCharacteristics": [ "Identifier", "InputDeviceType", - "TargetVisibilityState", - "Name" + "TargetVisibilityState" ], "RequiredCharacteristics": [ "ConfiguredName", "InputSourceType", "IsConfigured", + "Name", "CurrentVisibilityState" ], "UUID": "000000D9-0000-1000-8000-0026BB765291" }, "IrrigationSystem": { "OptionalCharacteristics": [ - "Name", "RemainingDuration", + "Name", "StatusFault" ], "RequiredCharacteristics": [ @@ -318,11 +505,11 @@ }, "LeakSensor": { "OptionalCharacteristics": [ + "Name", "StatusActive", "StatusFault", - "StatusTampered", "StatusLowBattery", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "LeakDetected" @@ -334,8 +521,8 @@ "Name", "StatusActive", "StatusFault", - "StatusTampered", - "StatusLowBattery" + "StatusLowBattery", + "StatusTampered" ], "RequiredCharacteristics": [ "CurrentAmbientLightLevel" @@ -345,11 +532,12 @@ "Lightbulb": { "OptionalCharacteristics": [ "Brightness", - "Hue", - "Saturation", - "Name", "ActiveTransitionCount", "TransitionControl", + "ColorTemperature", + "Hue", + "Name", + "Saturation", "SupportedTransitionConfiguration" ], "RequiredCharacteristics": [ @@ -359,12 +547,12 @@ }, "LockManagement": { "OptionalCharacteristics": [ - "Logs", + "AdministratorOnlyAccess", "AudioFeedback", + "CurrentDoorState", "LockManagementAutoSecurityTimeout", - "AdministratorOnlyAccess", "LockLastKnownAction", - "CurrentDoorState", + "Logs", "MotionDetected", "Name" ], @@ -389,8 +577,8 @@ "Name" ], "RequiredCharacteristics": [ - "Volume", - "Mute" + "Mute", + "Volume" ], "UUID": "00000112-0000-1000-8000-0026BB765291" }, @@ -400,20 +588,31 @@ "StatusFault", "StatusTampered", "StatusLowBattery", - "Name" + "Name", + "ContributingSensors", + "MotionEnabled" ], "RequiredCharacteristics": [ "MotionDetected" ], "UUID": "00000085-0000-1000-8000-0026BB765291" }, + "NFCAccess": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "ConfigurationState", + "NFCAccessControlPoint", + "NFCAccessSupportedConfiguration" + ], + "UUID": "00000266-0000-1000-8000-0026BB765291" + }, "OccupancySensor": { "OptionalCharacteristics": [ "Name", "StatusActive", "StatusFault", - "StatusTampered", - "StatusLowBattery" + "StatusLowBattery", + "StatusTampered" ], "RequiredCharacteristics": [ "OccupancyDetected" @@ -430,12 +629,39 @@ ], "UUID": "00000047-0000-1000-8000-0026BB765291" }, + "Pairing": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "PairingPairings", + "PairSetup", + "PairVerify", + "PairingFeatures" + ], + "UUID": "00000055-0000-1000-8000-0026BB765291" + }, + "PowerManagement": { + "OptionalCharacteristics": [ + "SelectedSleepConfiguration", + "SupportedSleepConfiguration" + ], + "RequiredCharacteristics": [ + "WakeConfiguration" + ], + "UUID": "00000221-0000-1000-8000-0026BB765291" + }, + "ProtocolInformation": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "Version" + ], + "UUID": "000000A2-0000-1000-8000-0026BB765291" + }, "SecuritySystem": { "OptionalCharacteristics": [ - "StatusFault", - "StatusTampered", + "Name", "SecuritySystemAlarmType", - "Name" + "StatusFault", + "StatusTampered" ], "RequiredCharacteristics": [ "SecuritySystemCurrentState", @@ -452,26 +678,51 @@ ], "UUID": "000000CC-0000-1000-8000-0026BB765291" }, + "Siri": { + "OptionalCharacteristics": [ + "MultifunctionButton", + "SiriEnable", + "SiriEngineVersion", + "SiriLightOnUse", + "SiriListening", + "SiriTouchToUse" + ], + "RequiredCharacteristics": [ + "SiriInputType" + ], + "UUID": "00000133-0000-1000-8000-0026BB765291" + }, + "SiriEndpoint": { + "OptionalCharacteristics": [ + "ActiveIdentifier", + "ManuallyDisabled" + ], + "RequiredCharacteristics": [ + "SiriEndpointSessionStatus", + "Version" + ], + "UUID": "00000253-0000-1000-8000-0026BB765291" + }, "Slat": { "OptionalCharacteristics": [ "Name", + "SwingMode", "CurrentTiltAngle", - "TargetTiltAngle", - "SwingMode" + "TargetTiltAngle" ], "RequiredCharacteristics": [ - "SlatType", - "CurrentSlatState" + "CurrentSlatState", + "SlatType" ], "UUID": "000000B9-0000-1000-8000-0026BB765291" }, "SmartSpeaker": { "OptionalCharacteristics": [ - "Name", - "ConfiguredName", "AirPlayEnable", - "Volume", - "Mute" + "ConfiguredName", + "Mute", + "Name", + "Volume" ], "RequiredCharacteristics": [ "CurrentMediaState", @@ -481,11 +732,11 @@ }, "SmokeSensor": { "OptionalCharacteristics": [ + "Name", "StatusActive", "StatusFault", - "StatusTampered", "StatusLowBattery", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "SmokeDetected" @@ -494,14 +745,27 @@ }, "Speaker": { "OptionalCharacteristics": [ - "Name", - "Volume" + "Active", + "Volume", + "VolumeControlType", + "VolumeSelector", + "Name" ], "RequiredCharacteristics": [ "Mute" ], "UUID": "00000113-0000-1000-8000-0026BB765291" }, + "StatefulProgrammableSwitch": { + "OptionalCharacteristics": [ + "Name" + ], + "RequiredCharacteristics": [ + "ProgrammableSwitchEvent", + "ProgrammableSwitchOutputState" + ], + "UUID": "00000088-0000-1000-8000-0026BB765291" + }, "StatelessProgrammableSwitch": { "OptionalCharacteristics": [ "Name", @@ -521,6 +785,25 @@ ], "UUID": "00000049-0000-1000-8000-0026BB765291" }, + "TargetControl": { + "OptionalCharacteristics": [ + "Name" + ], + "RequiredCharacteristics": [ + "Active", + "ActiveIdentifier", + "ButtonEvent" + ], + "UUID": "00000125-0000-1000-8000-0026BB765291" + }, + "TargetControlManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "SupportedTargetConfiguration", + "TargetListConfiguration" + ], + "UUID": "00000122-0000-1000-8000-0026BB765291" + }, "Television": { "OptionalCharacteristics": [ "Brightness", @@ -528,14 +811,15 @@ "DisplayOrder", "CurrentMediaState", "TargetMediaState", + "Name", "PictureMode", - "PowerModeSelection", - "RemoteKey" + "PowerModeSelection" ], "RequiredCharacteristics": [ "Active", "ActiveIdentifier", "ConfiguredName", + "RemoteKey", "SleepDiscoveryMode" ], "UUID": "000000D8-0000-1000-8000-0026BB765291" @@ -555,11 +839,11 @@ }, "TemperatureSensor": { "OptionalCharacteristics": [ + "Name", "StatusActive", "StatusFault", "StatusLowBattery", - "StatusTampered", - "Name" + "StatusTampered" ], "RequiredCharacteristics": [ "CurrentTemperature" @@ -568,11 +852,11 @@ }, "Thermostat": { "OptionalCharacteristics": [ + "Name", "CurrentRelativeHumidity", "TargetRelativeHumidity", "CoolingThresholdTemperature", - "HeatingThresholdTemperature", - "Name" + "HeatingThresholdTemperature" ], "RequiredCharacteristics": [ "CurrentHeatingCoolingState", @@ -583,14 +867,45 @@ ], "UUID": "0000004A-0000-1000-8000-0026BB765291" }, + "ThreadTransport": { + "OptionalCharacteristics": [ + "CCAEnergyDetectThreshold", + "CCASignalDetectThreshold", + "EventRetransmissionMaximum", + "EventTransmissionCounters", + "MACRetransmissionMaximum", + "MACTransmissionCounters", + "ReceiverSensitivity", + "ReceivedSignalStrengthIndication", + "SignalToNoiseRatio", + "ThreadOpenThreadVersion", + "TransmitPower", + "MaximumTransmitPower" + ], + "RequiredCharacteristics": [ + "CurrentTransport", + "ThreadControlPoint", + "ThreadNodeCapabilities", + "ThreadStatus" + ], + "UUID": "00000701-0000-1000-8000-0026BB765291" + }, + "TransferTransportManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "SupportedTransferTransportConfiguration", + "SetupTransferTransport" + ], + "UUID": "00000203-0000-1000-8000-0026BB765291" + }, "Valve": { "OptionalCharacteristics": [ - "SetDuration", - "RemainingDuration", "IsConfigured", + "Name", + "RemainingDuration", "ServiceLabelIndex", - "StatusFault", - "Name" + "SetDuration", + "StatusFault" ], "RequiredCharacteristics": [ "Active", @@ -599,43 +914,67 @@ ], "UUID": "000000D0-0000-1000-8000-0026BB765291" }, + "WiFiRouter": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "ConfiguredName", + "ManagedNetworkEnable", + "NetworkAccessViolationControl", + "NetworkClientControl", + "NetworkClientStatusControl", + "RouterStatus", + "SupportedRouterConfiguration", + "WANConfigurationList", + "WANStatusList" + ], + "UUID": "0000020A-0000-1000-8000-0026BB765291" + }, + "WiFiSatellite": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "WiFiSatelliteStatus" + ], + "UUID": "0000020F-0000-1000-8000-0026BB765291" + }, + "WiFiTransport": { + "OptionalCharacteristics": [ + "PreferredWiFiList", + "WiFiConfigurationControl" + ], + "RequiredCharacteristics": [ + "CurrentTransport", + "WiFiCapabilities" + ], + "UUID": "0000022A-0000-1000-8000-0026BB765291" + }, "Window": { "OptionalCharacteristics": [ - "HoldPosition", + "Name", "ObstructionDetected", - "Name" + "HoldPosition" ], "RequiredCharacteristics": [ "CurrentPosition", - "TargetPosition", - "PositionState" + "PositionState", + "TargetPosition" ], "UUID": "0000008B-0000-1000-8000-0026BB765291" }, "WindowCovering": { "OptionalCharacteristics": [ - "HoldPosition", - "TargetHorizontalTiltAngle", - "TargetVerticalTiltAngle", "CurrentHorizontalTiltAngle", - "CurrentVerticalTiltAngle", + "TargetHorizontalTiltAngle", + "Name", "ObstructionDetected", - "Name" + "HoldPosition", + "CurrentVerticalTiltAngle", + "TargetVerticalTiltAngle" ], "RequiredCharacteristics": [ "CurrentPosition", - "TargetPosition", - "PositionState" + "PositionState", + "TargetPosition" ], "UUID": "0000008C-0000-1000-8000-0026BB765291" - }, - "NFCAccess": { - "OptionalCharacteristics": [], - "RequiredCharacteristics": [ - "ConfigurationState", - "NFCAccessControlPoint", - "NFCAccessSupportedConfiguration" - ], - "UUID": "00000266-0000-1000-8000-0026BB765291" } } diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index d0217303..3aaf2a69 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -1,123 +1,516 @@ #!/usr/bin/env python3 -"""Create a json representation from the HomeKit Accessory Simulator Types.""" -import plistlib +"""Generate pyhap/resources/characteristics.json and services.json from Apple metadata. + +HOW THE PROCESS WORKS +==================== + +This script keeps HAP-python's characteristic/service catalog in sync with Apple's +official HomeKit definitions without hand-editing JSON files. It mirrors the +approach used by HAP-NodeJS (homebridge/HAP-NodeJS, `src/lib/definitions/ +generate-definitions.ts`). + +Sources, in order of increasing precedence: + +1. plain-metadata.config (PRIMARY, always the newest) + Located at: + /System/Library/PrivateFrameworks/HomeKitDaemon.framework/Resources/plain-metadata.config + This file ships WITH macOS and is updated with every OS release, so it does not + require Xcode or the HomeKit Accessory Simulator to be installed. It is a plain + binary plist and contains, under PlistDictionary -> HAP: + - Characteristics: "" -> { DefaultDescription, Format, Properties (bitmask), + ShortUUID, MinValue/MaxValue/StepValue, Units, ... } + - Services: "" -> { Characteristics: {Required: [...], Optional: [...]}, + ShortUUID, DefaultDescription } + - Units, Properties (bit position legend for the Properties bitmask), Base UUID. + +2. HomeKit Accessory Simulator plists (OPTIONAL OVERLAY, needs Xcode tools) + Located inside the "HomeKit Accessory Simulator.app" (see SIMULATOR_PLIST below). + The simulator metadata lags behind plain-metadata.config, but it carries two + things plain-metadata encodes poorly or not at all: + - Permissions in string form ("read", "write", "cnotify", "timedWrite", + "writeResponse", "uncnotify", ...) - richer than the bitmask. + - ValidValues constraints for enumerated characteristics. + When the simulator is not installed the script still works: permissions fall + back to the Properties bitmask and ValidValues fall back to source 4 below. + +3. scripts/hap_manual_overrides.json (MANUAL OVERRIDES - the maintenance escape hatch) + Apple's machine-readable metadata only covers definitions that have SHIPPED in + an OS release. Things published only in a developer-preview document (e.g. the + HomeKit Secure Video 17.99 Open Source Compatibility Guide, which is where the + CameraBufferManagement / CameraWebRTCStreamManagement / ... 0x80xx definitions + come from) do not appear in any plist yet. Those must be added by hand - but only + ONCE, here, instead of re-parsing the PDF guide every time. When Apple eventually + ships them in plain-metadata.config, the manual entry can simply be deleted. + The overrides file supports: + characteristicNameOverrides slug -> HAP-python name (when the derived name is wrong) + characteristicAdditions new characteristics, same schema as characteristics.json + characteristicOverrides full replacement of a generated characteristic entry + serviceAdditions new services, same schema as services.json + serviceOverrides full replacement of a generated service entry + +4. Previously generated output (FIELD INHERITANCE) + When the simulator is unavailable, some fields (most importantly "ValidValues", + occasionally "maxLen"/"unit") cannot be derived from plain-metadata.config. To + avoid silently dropping them, fields present in the existing + characteristics.json / services.json entry (matched by UUID) are carried over + when the freshly generated entry is missing them. This keeps the catalog + stable across regenerations on machines without the simulator. + +OUTPUT +====== +By default the script runs as a DRY RUN: it prints a report of what would be +added/removed/changed relative to the committed JSON files. Pass --write to +actually rewrite them. Always review the diff before committing. + +The old version of this script parsed ONLY the HomeKit Accessory Simulator plist; +it has been rewritten because that plist lags far behind (163 characteristics +vs 242+ in plain-metadata.config at the time of writing) and requires Xcode. +""" + +import argparse import json +import plistlib +import sys +from pathlib import Path + +# --------------------------------------------------------------------------- +# Source locations +# --------------------------------------------------------------------------- + +# Primary source: ships with macOS, updated with every OS release. +PLAIN_METADATA_PATH = Path( + "/System/Library/PrivateFrameworks/HomeKitDaemon.framework/Resources/plain-metadata.config" +) -# This path could be different. -HOMEKIT_TYPES_PLIST = "/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist" -CHAR_OUT_FILE = "./pyhap/resources/characteristics.json" -SERVICE_OUT_FILE = "./pyhap/resources/services.json" +# Optional overlay: only present when Xcode's HomeKit Accessory Simulator is installed. +SIMULATOR_PLIST = Path( + "/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app/" + "Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist" +) + +REPO_ROOT = Path(__file__).resolve().parent.parent +CHAR_OUT_FILE = REPO_ROOT / "pyhap/resources/characteristics.json" +SERVICE_OUT_FILE = REPO_ROOT / "pyhap/resources/services.json" +MANUAL_OVERRIDES_FILE = Path(__file__).resolve().parent / "hap_manual_overrides.json" + +# --------------------------------------------------------------------------- +# Mapping tables +# --------------------------------------------------------------------------- + +# plain-metadata.config encodes characteristic permissions as a bitmask over +# HAP.Properties (key -> bit position). Decode into HAP-python permission +# strings. Bits HAP-python does not understand (aa, broadcast, hidden via bit, +# writeResponse) are handled separately below. +PERMISSION_BITS = { + "read": "pr", + "write": "pw", + "notify": "ev", + "timedWrite": "tw", + "writeResponse": "wr", +} -PERMS_MAP = { +# Simulator plist permission strings -> HAP-python permission strings. +# (Kept from the previous version of this script; "uncnotify" has no +# HAP-python representation and is skipped.) +SIMULATOR_PERMS_MAP = { "read": "pr", "write": "pw", "cnotify": "ev", - # TODO: find the 'symbol' for this one - "uncnotify": None, + "timedWrite": "tw", + "writeResponse": "wr", } -CONSTRAINTS_MAP = { +# Simulator plist constraint keys -> HAP-python characteristic field names. +SIMULATOR_CONSTRAINTS_MAP = { "MaximumValue": "maxValue", "MinimumValue": "minValue", "StepValue": "minStep", + "MaxLength": "maxLen", +} + +# plain-metadata.config char fields -> HAP-python characteristic field names. +METADATA_FIELDS_MAP = { + "MaxValue": "maxValue", + "MinValue": "minValue", + "StepValue": "minStep", + "MaxLength": "maxLen", + "Units": "unit", } +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def to_hap_name(description): + """Convert a human-readable description to the HAP-python catalog name. + + Apple metadata uses e.g. "Current Ambient Light Level" or "PM2.5 Density", + while the JSON catalog uses the spaces-stripped form ("PM25Density"). + Hyphens ("Wi-Fi", "Heater-Cooler") are stripped as well so newly added + entries follow the same convention. + """ + return description.replace(" ", "").replace("-", "") + + +def long_uuid(short_uuid, base_uuid="-0000-1000-8000-0026BB765291"): + """Expand a short UUID (e.g. "000000B0") into the full HAP UUID form.""" + return f"{short_uuid}{base_uuid}" + + +def perms_from_bitmask(properties, hap_properties): + """Decode the plain-metadata Properties bitmask into HAP-python permissions. + + `properties` is the integer bitmask; `hap_properties` is the HAP.Properties + legend (name -> {"Position": n}) from the same file. + Admin-only characteristics (the "adminOnly" bit) are recorded separately in + "AdminOnlyAccess": ["write"] in the output, mirroring HAP-NodeJS; HAP-python + does not enforce this, it is informational. + """ + perms = [] + admin_only = False + for name, info in hap_properties.items(): + if not properties & (1 << info["Position"]): + continue + if name == "adminOnly": + admin_only = True + elif name in PERMISSION_BITS: + perms.append(PERMISSION_BITS[name]) + # Keep a stable, conventional order. + order = {"pr": 0, "pw": 1, "ev": 2, "tw": 3, "wr": 4} + perms.sort(key=lambda p: order.get(p, 99)) + return perms, admin_only + + +# --------------------------------------------------------------------------- +# Source parsers +# --------------------------------------------------------------------------- + + +def load_plain_metadata(path): + """Parse plain-metadata.config and return (characteristics, services, legend). -def create_uuid2name_map(char_info): - """Return a mapping UUIDs to Names.""" - uuid2name = {} - for char in char_info: - uuid2name[char["UUID"]] = char["Name"] - return uuid2name + Returns raw Apple dicts keyed by slug: + characteristics: slug -> {DefaultDescription, Format, Properties, ShortUUID, ...} + services: slug -> {ShortUUID, Characteristics: {Required, Optional}, ...} + units: slug -> DefaultDescription (e.g. "percentage" -> "Percentage") + hap_properties: the HAP.Properties bitmask legend + """ + with path.open("rb") as fp: + metadata = plistlib.load(fp) + hap = metadata["PlistDictionary"]["HAP"] + return ( + hap["Characteristics"], + hap["Services"], + hap["Units"], + hap["Properties"], + ) + + +def load_simulator_plist(path): + """Parse the HomeKit Accessory Simulator plist (optional overlay). + + Returns {UUID -> char entry} with Name, Format, Permissions, Constraints. + The simulator keys entries by full UUID; the Permissions strings are richer + than the plain-metadata bitmask and Constraints carry ValidValues. + """ + with path.open("rb") as fp: + data = plistlib.load(fp) + chars = {} + for char in data.get("Characteristics", []): + chars[char["UUID"].upper()] = char + return chars def fix_valid_values(char_info): """Valid values are given in a value: key format. Reverse them.""" - for char in char_info: + for char in char_info.values(): if "ValidValues" not in char: continue valid_values = {} for value, state in char["ValidValues"].items(): - if "int" in char["Format"]: + if "int" in char["Format"] or char["Format"].startswith("uint"): value = int(value) elif "float" == char["Format"]: value = float(value) - else: - raise ValueError state = state.replace(" ", "") # To camel case. valid_values[state] = value char["ValidValues"] = valid_values -def tidy_char(char_info): - """Various things we would like to change about a Characteristic representation.""" - for char in char_info: - if "Unit" in char: - char["unit"] = char.pop("Unit") - if "Properties" in char: - permissions = [] - for perm in char.pop("Properties"): - if perm == "uncnotify": +# --------------------------------------------------------------------------- +# Generation +# --------------------------------------------------------------------------- + + +def generate_characteristics(meta_chars, units, hap_properties, sim_chars, + manual, existing_chars, name_overrides): + """Build the HAP-python characteristics catalog. + + Steps: + 1. Convert every plain-metadata characteristic to catalog form. + 2. Overlay simulator Permissions/ValidValues/constraints where available. + 3. Inherit fields (ValidValues etc.) from the previously generated file. + 4. Apply manual overrides and additions last (highest precedence). + Returns (catalog, slug_to_final) where slug_to_final maps each Apple slug + to the FINAL catalog name (after UUID-based name resolution). Service + definitions reference characteristics by slug, so this map is required to + resolve them to names that actually exist in the catalog. + """ + catalog = {} + slug_to_final = {} + + # Names: prefer the name already used in the committed catalog for a given + # UUID so that regenerating never renames an existing characteristic + # (Apple's descriptions drift over time, e.g. "CarbonDioxideDetected" vs + # "Carbon dioxide Detected"). New UUIDs get a name derived from the + # description, with any manual override taking precedence. + uuid_to_name = {} + for prev_name, prev in existing_chars.items(): + uuid_to_name.setdefault(prev.get("UUID", "").upper(), prev_name) + for slug, info in meta_chars.items(): + uuid = long_uuid(info["ShortUUID"]) + name = (name_overrides.get(slug) + or uuid_to_name.get(uuid.upper()) + or to_hap_name(info["DefaultDescription"])) + slug_to_final[slug] = name + entry = { + "Format": info["Format"], + "Permissions": None, # filled below + "UUID": long_uuid(info["ShortUUID"]), + } + for meta_key, out_key in METADATA_FIELDS_MAP.items(): + if meta_key in info: + entry[out_key] = info[meta_key] + + perms, admin_only = perms_from_bitmask(info["Properties"], hap_properties) + + # Simulator overlay: richer permission strings + constraints. + sim = sim_chars.get(entry["UUID"].upper()) + if sim: + perms = [SIMULATOR_PERMS_MAP[p] for p in sim.get("Permissions", []) + if p in SIMULATOR_PERMS_MAP] + constraints = sim.get("Constraints") or {} + for sim_key, out_key in SIMULATOR_CONSTRAINTS_MAP.items(): + if sim_key in constraints: + entry[out_key] = constraints[sim_key] + + entry["Permissions"] = perms + if admin_only: + entry["AdminOnlyAccess"] = ["write"] + + # Field inheritance from the previously generated catalog (matched by + # UUID) for anything plain-metadata cannot express. + prev = existing_chars.get(name) + if prev and prev.get("UUID") == entry["UUID"]: + for key, value in prev.items(): + if key not in entry and key != "ValidValues": + entry[key] = value + catalog[name] = entry + + if sim_chars: + # Apply ValidValues conversion on the overlay entries. + for name, entry in catalog.items(): + sim = sim_chars.get(entry["UUID"].upper()) + if sim and "ValidValues" in (sim.get("Constraints") or {}): + c = {"ValidValues": sim["Constraints"]["ValidValues"], + "Format": entry["Format"]} + fix_valid_values(c) + entry["ValidValues"] = c["ValidValues"] + else: + # No simulator: inherit ValidValues from the previous catalog. + for name, entry in catalog.items(): + prev = existing_chars.get(name) + if prev and prev.get("UUID") == entry["UUID"] and "ValidValues" in prev: + entry["ValidValues"] = prev["ValidValues"] + + # If a characteristic has enumerated ValidValues, drop the numeric + # min/max/step constraints. HAP-python clamps values to min/max BEFORE + # checking ValidValues, so keeping both would turn invalid values into + # silent clamping (e.g. a stored 7 for Active would clamp to maxValue=1 + # and read back as "active") instead of being rejected. + for entry in catalog.values(): + if "ValidValues" in entry: + for key in ("maxValue", "minValue", "minStep"): + entry.pop(key, None) + + # Manual overrides and additions (highest precedence). + for name, entry in manual.get("characteristicOverrides", {}).items(): + catalog[name] = entry + for name, entry in manual.get("characteristicAdditions", {}).items(): + catalog[name] = entry + + return catalog, slug_to_final + + +def generate_services(meta_services, characteristics, manual, name_overrides, + existing_services): + """Build the HAP-python services catalog from plain-metadata + manual files. + + plain-metadata references characteristics by slug; each slug is translated + to the catalog name computed in generate_characteristics(). Slugs that are + unknown (dropped from Apple metadata) are reported and skipped. + """ + catalog = {} + + # Same UUID-based naming rule as for characteristics: services keep the + # name already used in the committed catalog (e.g. "Slat" instead of + # Apple's newer "Slats" description for UUID 0x000000B9). + uuid_to_name = {} + for prev_name, prev in existing_services.items(): + uuid_to_name.setdefault(prev.get("UUID", "").upper(), prev_name) + for slug, info in meta_services.items(): + uuid = long_uuid(info["ShortUUID"]) + name = uuid_to_name.get(uuid.upper()) or to_hap_name(info["DefaultDescription"]) + # Resolve the characteristic slugs referenced by the service into + # catalog names using the slug -> name table (name_overrides["_slug_to_name"] + # plus any manual name overrides). + def resolve(slug_c): + return (name_overrides.get(slug_c) + or name_overrides.get("_slug_to_name", {}).get(slug_c)) + + req_opt = info["Characteristics"] + entry = { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [], + "UUID": long_uuid(info["ShortUUID"]), + } + unknown = [] + for slug_c in req_opt.get("Optional", []): + cname = resolve(slug_c) + if cname and cname in characteristics: + entry["OptionalCharacteristics"].append(cname) + else: + unknown.append(slug_c) + for slug_c in req_opt.get("Required", []): + cname = resolve(slug_c) + if cname and cname in characteristics: + entry["RequiredCharacteristics"].append(cname) + else: + unknown.append(slug_c) + if unknown: + print(f" ! service {name}: unknown characteristics {unknown}", file=sys.stderr) + + # COMPATIBILITY INHERITANCE from the previously generated catalog: + # pyhap's Service() auto-instantiates the REQUIRED characteristics at + # runtime, so a required->optional demotion in Apple's metadata would + # silently remove characteristics from existing accessories. Apple + # occasionally does this (e.g. FirmwareRevision on AccessoryInformation) + # while the HAP specification still lists them as required. Rule: + # - a characteristic that was Required stays Required; + # - a characteristic that was Optional stays Optional (allowed lists + # only ever grow). + # Genuine removals must be done explicitly via a serviceOverride entry + # in the manual overrides file. + prev = existing_services.get(name) + if prev and prev.get("UUID") == entry["UUID"]: + # Required characteristics that were demoted to Optional by Apple + # are re-promoted to Required. + for c in prev.get("RequiredCharacteristics", []): + if c not in characteristics or c in entry["RequiredCharacteristics"]: continue - permissions.append(PERMS_MAP[perm]) - char["Permissions"] = permissions - if char["Format"] == "int32": - char["Format"] = "int" - if "Constraints" in char: - constraints = char.pop("Constraints") - for key, value in constraints.items(): - char[CONSTRAINTS_MAP.get(key, key)] = value - - -def replace_char_uuid(service_info, uuid2name): - """Replace characteristics' UUID with their name for convenience.""" - for service in service_info: - if "RequiredCharacteristics" in service: - req_chars = [] - for char in service["RequiredCharacteristics"]: - req_chars.append(uuid2name[char]) - service["RequiredCharacteristics"] = req_chars - if "OptionalCharacteristics" in service: - opt_chars = [] - for char in service.get("OptionalCharacteristics", []): - opt_chars.append(uuid2name[char]) - service["OptionalCharacteristics"] = opt_chars - - -def camel_name(infos): - """Transform the name to camel case, no spaces.""" - for info in infos: - info["Name"] = info["Name"].replace(" ", "") - - -def list2dict(infos): - """We want a mapping name: char/service for convenience, not a list.""" - info_dict = {} - for info in infos: - info_dict[info["Name"]] = info - del info["Name"] - return info_dict + entry["RequiredCharacteristics"].append(c) + if c in entry["OptionalCharacteristics"]: + entry["OptionalCharacteristics"].remove(c) + # Optional characteristics that were dropped entirely are re-added + # (the allowed list only ever grows). + for c in prev.get("OptionalCharacteristics", []): + if c in characteristics and c not in entry["RequiredCharacteristics"] \ + and c not in entry["OptionalCharacteristics"]: + entry["OptionalCharacteristics"].append(c) + catalog[name] = entry + + for name, entry in manual.get("serviceOverrides", {}).items(): + catalog[name] = entry + for name, entry in manual.get("serviceAdditions", {}).items(): + catalog[name] = entry + + return catalog + + +# --------------------------------------------------------------------------- +# Reporting / main +# --------------------------------------------------------------------------- + + +def diff_report(old, new, label): + added = sorted(set(new) - set(old)) + removed = sorted(set(old) - set(new)) + changed = sorted(k for k in set(new) & set(old) if new[k] != old[k]) + print(f"{label}: {len(new)} entries ({len(added)} added, {len(removed)} removed, " + f"{len(changed)} changed)") + for k in added: + print(f" + {k} ({new[k].get('UUID')})") + for k in removed: + print(f" - {k} ({old[k].get('UUID')})") + return added, removed, changed def main(): - """Reads the HomeKit Simulator types and creates a HAP-python json representation.""" - with open(HOMEKIT_TYPES_PLIST, "rb") as types_plist_fp: - type_info = plistlib.load(types_plist_fp) - char_info = type_info["Characteristics"] - service_info = type_info["Services"] + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--plain-metadata", type=Path, default=PLAIN_METADATA_PATH, + help="Path to plain-metadata.config") + parser.add_argument("--simulator-plist", type=Path, default=SIMULATOR_PLIST, + help="Path to the HomeKit Accessory Simulator default.metadata.plist") + parser.add_argument("--manual-overrides", type=Path, default=MANUAL_OVERRIDES_FILE, + help="Path to hap_manual_overrides.json") + parser.add_argument("--write", action="store_true", + help="Write the generated files (default is a dry-run report)") + args = parser.parse_args() + + if not args.plain_metadata.exists(): + sys.exit(f"Primary metadata not found at {args.plain_metadata}. " + "This script must run on macOS.") + + manual = json.loads(args.manual_overrides.read_text()) + + meta_chars, meta_services, _units, hap_properties = load_plain_metadata(args.plain_metadata) + print(f"plain-metadata.config: {len(meta_chars)} characteristics, " + f"{len(meta_services)} services") + + sim_chars = {} + if args.simulator_plist.exists(): + sim_chars = load_simulator_plist(args.simulator_plist) + print(f"simulator plist: {len(sim_chars)} characteristics (overlay enabled)") + else: + print("simulator plist: not found - using bitmask permissions and " + "inherited ValidValues") + + existing_chars = json.loads(CHAR_OUT_FILE.read_text()) + existing_services = json.loads(SERVICE_OUT_FILE.read_text()) - camel_name(char_info) - camel_name(service_info) + # slug -> HAP-python name, used to resolve the characteristic references + # inside service definitions. + # Base slug -> name table from descriptions. Used for legacy slugs that + # Apple still references in service definitions but no longer ships as + # characteristic entries (e.g. "on" when the UUID 0x25 entry is now + # described as "PowerState"). The FINAL names recorded during generation + # (below) take precedence over this description-derived fallback. + slug_to_name = { + slug: to_hap_name(info["DefaultDescription"]) + for slug, info in meta_chars.items() + } + name_overrides = dict(manual.get("characteristicNameOverrides", {})) + name_overrides["_slug_to_name"] = slug_to_name - uuid2name = create_uuid2name_map(char_info) + new_chars, slug_to_final = generate_characteristics( + meta_chars, _units, hap_properties, sim_chars, manual, existing_chars, + name_overrides) + name_overrides["_slug_to_name"].update(slug_to_final) + new_services = generate_services( + meta_services, new_chars, manual, name_overrides, existing_services) - tidy_char(char_info) - fix_valid_values(char_info) - with open(CHAR_OUT_FILE, "w") as char_fp: - json.dump(list2dict(char_info), char_fp, indent=3, sort_keys=True) + print() + diff_report(existing_chars, new_chars, "characteristics.json") + print() + diff_report(existing_services, new_services, "services.json") - replace_char_uuid(service_info, uuid2name) - with open(SERVICE_OUT_FILE, "w") as services_fp: - json.dump(list2dict(service_info), services_fp, indent=3, sort_keys=True) + if args.write: + CHAR_OUT_FILE.write_text(json.dumps(new_chars, indent=3, sort_keys=True) + "\n") + SERVICE_OUT_FILE.write_text(json.dumps(new_services, indent=3, sort_keys=True) + "\n") + print("\nWrote", CHAR_OUT_FILE, "and", SERVICE_OUT_FILE) + else: + print("\nDry run - nothing written. Re-run with --write to update the files.") if __name__ == "__main__": diff --git a/scripts/hap_manual_overrides.json b/scripts/hap_manual_overrides.json new file mode 100644 index 00000000..b0a40605 --- /dev/null +++ b/scripts/hap_manual_overrides.json @@ -0,0 +1,437 @@ +{ + "_comment": [ + "Manual additions and overrides for scripts/gen_hap_types.py.", + "Apple's machine-readable metadata (HomeKitDaemon plain-metadata.config and the", + "HomeKit Accessory Simulator plists) only covers characteristics and services that", + "have shipped in an OS release. Things published only in a developer-preview spec", + "(e.g. the HomeKit Secure Video 17.99 Open Source Compatibility Guide) must be", + "added by hand here until they show up in Apple's metadata.", + "Keys must use the same schema as pyhap/resources/characteristics.json and", + "pyhap/resources/services.json. Overrides replace the generated entry entirely.", + "Entries listed under additions here are also used to RETAIN definitions", + "that Apple dropped from plain-metadata.config but the HAP spec still defines", + "(TargetSlatState, TargetAirQuality, TelevisionSpeaker). Delete them once", + "Apple ships them again. NOTE: the generator never removes a required or", + "optional characteristic from an existing service - demotions in Apple's", + "metadata are ignored by design; use serviceOverrides to remove one deliberately." + ], + "characteristicAdditions": { + "BufferActivityCommand": { + "Format": "tlv8", + "Permissions": [ + "pw" + ], + "UUID": "00008017-0000-1000-8000-0026BB765291" + }, + "BufferEventCommand": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008014-0000-1000-8000-0026BB765291" + }, + "BufferEventSequenceNumber": { + "Format": "uint32", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008015-0000-1000-8000-0026BB765291" + }, + "BufferUploadCommand": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008013-0000-1000-8000-0026BB765291" + }, + "CameraCapabilities": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00008011-0000-1000-8000-0026BB765291" + }, + "CameraClientCSR": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008081-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificate": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "tw" + ], + "UUID": "00008082-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificateStatus": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008083-0000-1000-8000-0026BB765291" + }, + "CameraKey": { + "Format": "tlv8", + "Permissions": [ + "pw", + "tw" + ], + "UUID": "00008051-0000-1000-8000-0026BB765291" + }, + "CameraKeyID": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008052-0000-1000-8000-0026BB765291" + }, + "CameraRecordingPublishingPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00008016-0000-1000-8000-0026BB765291" + }, + "CameraZones": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw" + ], + "UUID": "00008022-0000-1000-8000-0026BB765291" + }, + "ContributingSensors": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008086-0000-1000-8000-0026BB765291" + }, + "MotionEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008087-0000-1000-8000-0026BB765291" + }, + "RTPStreamingControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008045-0000-1000-8000-0026BB765291" + }, + "SensorUUID": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "0000805B-0000-1000-8000-0026BB765291" + }, + "StreamingEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008041-0000-1000-8000-0026BB765291" + }, + "SupportedAudioStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008044-0000-1000-8000-0026BB765291" + }, + "SupportedVideoStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008043-0000-1000-8000-0026BB765291" + }, + "TargetAirQuality": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "000000AE-0000-1000-8000-0026BB765291", + "ValidValues": { + "Excellent": 0, + "Fair": 2, + "Good": 1 + } + }, + "TargetSlatState": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev" + ], + "UUID": "000000BE-0000-1000-8000-0026BB765291", + "ValidValues": { + "Auto": 1, + "Manual": 0 + } + }, + "WebRTCNumberOfActiveSessions": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008057-0000-1000-8000-0026BB765291", + "maxValue": 255, + "minValue": 0 + }, + "WebRTCProvideAnswer": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008054-0000-1000-8000-0026BB765291" + }, + "WebRTCReoffer": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008058-0000-1000-8000-0026BB765291" + }, + "WebRTCSolicitOffer": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008053-0000-1000-8000-0026BB765291" + }, + "WebRTCStreamingControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008056-0000-1000-8000-0026BB765291" + }, + "WebRTCSupportedAudioStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000805A-0000-1000-8000-0026BB765291" + }, + "WebRTCSupportedVideoStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008059-0000-1000-8000-0026BB765291" + }, + "WebRTCUpdateSession": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "0000805C-0000-1000-8000-0026BB765291" + } + }, + "characteristicNameOverrides": { + "snr": "SignalToNoiseRatio" + }, + "characteristicOverrides": { + "CameraOperatingModeIndicator": { + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "0000021D-0000-1000-8000-0026BB765291" + }, + "NightVision": { + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "0000011B-0000-1000-8000-0026BB765291" + }, + "ThirdPartyCameraActive": { + "Format": "bool", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000021C-0000-1000-8000-0026BB765291" + } + }, + "serviceAdditions": { + "CameraBufferManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "BufferUploadCommand", + "BufferActivityCommand", + "BufferEventCommand", + "BufferEventSequenceNumber", + "CameraRecordingPublishingPoint" + ], + "UUID": "00008000-0000-1000-8000-0026BB765291" + }, + "CameraCapabilities": { + "OptionalCharacteristics": [ + "ManuallyDisabled" + ], + "RequiredCharacteristics": [ + "Version", + "CameraCapabilities" + ], + "UUID": "00008010-0000-1000-8000-0026BB765291" + }, + "CameraClientCertificateManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "CameraClientCSR", + "CameraClientCertificate", + "CameraClientCertificateStatus" + ], + "UUID": "00008080-0000-1000-8000-0026BB765291" + }, + "CameraGlobalOperatingMode": { + "OptionalCharacteristics": [ + "ManuallyDisabled", + "NightVision", + "ThirdPartyCameraActive" + ], + "RequiredCharacteristics": [ + "HomeKitCameraActive", + "StreamingEnabled", + "CameraOperatingModeIndicator" + ], + "UUID": "00008032-0000-1000-8000-0026BB765291" + }, + "CameraKeyManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "CameraKey", + "CameraKeyID" + ], + "UUID": "00008050-0000-1000-8000-0026BB765291" + }, + "CameraMotionZones": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "Version", + "Active", + "CameraZones" + ], + "UUID": "00008021-0000-1000-8000-0026BB765291" + }, + "CameraMultiTierRTPStreamManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "StreamingEnabled", + "StatusActive", + "SupportedVideoStreamTiers", + "SupportedAudioStreamTiers", + "SupportedRTPConfiguration", + "SetupEndpoints", + "RTPStreamingControl", + "SensorUUID" + ], + "UUID": "00008031-0000-1000-8000-0026BB765291" + }, + "CameraWebRTCStreamManagement": { + "OptionalCharacteristics": [], + "RequiredCharacteristics": [ + "WebRTCSolicitOffer", + "WebRTCProvideAnswer", + "WebRTCStreamingControl", + "WebRTCNumberOfActiveSessions", + "WebRTCReoffer", + "WebRTCUpdateSession", + "WebRTCSupportedVideoStreamTiers", + "WebRTCSupportedAudioStreamTiers", + "StreamingEnabled", + "SensorUUID" + ], + "UUID": "00008033-0000-1000-8000-0026BB765291" + }, + "TelevisionSpeaker": { + "OptionalCharacteristics": [ + "Active", + "Volume", + "VolumeControlType", + "VolumeSelector", + "Name" + ], + "RequiredCharacteristics": [ + "Mute" + ], + "UUID": "00000113-0000-1000-8000-0026BB765291" + } + }, + "serviceOverrides": { + "MotionSensor": { + "OptionalCharacteristics": [ + "StatusActive", + "StatusFault", + "StatusTampered", + "StatusLowBattery", + "Name", + "ContributingSensors", + "MotionEnabled" + ], + "RequiredCharacteristics": [ + "MotionDetected" + ], + "UUID": "00000085-0000-1000-8000-0026BB765291" + } + } +} \ No newline at end of file From d5629571dece10f85ea008d9e341c1400c058006 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 09:56:38 +0200 Subject: [PATCH 2/7] gen_hap_types: require simulator plist, add --allow-missing-simulator Error out when the HomeKit Accessory Simulator plist is missing instead of silently degrading: without the overlay, ValidValues for newly added characteristics would be missing and permissions fall back to the coarser bitmask. --allow-missing-simulator proceeds explicitly with a loud warning; existing characteristics remain covered by field inheritance. --- scripts/gen_hap_types.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index 3aaf2a69..f667ab48 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -62,6 +62,14 @@ added/removed/changed relative to the committed JSON files. Pass --write to actually rewrite them. Always review the diff before committing. +The simulator overlay is REQUIRED by default: without it, ValidValues for newly +added Apple characteristics would be silently missing and permissions fall +back to the coarser bitmask. If the HomeKit Accessory Simulator is not +installed, the script errors out - pass --allow-missing-simulator to proceed +explicitly (existing characteristics are still covered by field inheritance +from the previously generated files, but review the report for entries +missing ValidValues). + The old version of this script parsed ONLY the HomeKit Accessory Simulator plist; it has been rewritten because that plist lags far behind (163 characteristics vs 242+ in plain-metadata.config at the time of writing) and requires Xcode. @@ -456,6 +464,10 @@ def main(): help="Path to hap_manual_overrides.json") parser.add_argument("--write", action="store_true", help="Write the generated files (default is a dry-run report)") + parser.add_argument("--allow-missing-simulator", action="store_true", + help="Proceed without the HomeKit Accessory Simulator " + "plist (ValidValues for new characteristics will be " + "missing; permissions use the bitmask).") args = parser.parse_args() if not args.plain_metadata.exists(): @@ -472,9 +484,18 @@ def main(): if args.simulator_plist.exists(): sim_chars = load_simulator_plist(args.simulator_plist) print(f"simulator plist: {len(sim_chars)} characteristics (overlay enabled)") + elif args.allow_missing_simulator: + print("WARNING: simulator plist not found - proceeding without the " + "overlay (--allow-missing-simulator). ValidValues for newly added " + "characteristics will be missing; permissions use the coarser " + "bitmask.", file=sys.stderr) else: - print("simulator plist: not found - using bitmask permissions and " - "inherited ValidValues") + sys.exit( + f"Simulator plist not found at {args.simulator_plist}.\n" + "Without it, ValidValues for newly added characteristics are missing " + "and permissions fall back to the coarser bitmask.\n" + "Install Xcode's HomeKit Accessory Simulator, or pass " + "--allow-missing-simulator to proceed explicitly.") existing_chars = json.loads(CHAR_OUT_FILE.read_text()) existing_services = json.loads(SERVICE_OUT_FILE.read_text()) From 934e7c9a78be1fb8b37464209a2614efc2c292ff Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 09:59:19 +0200 Subject: [PATCH 3/7] loudly fail if simulator is missing --- scripts/gen_hap_types.py | 150 ++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 49 deletions(-) diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index f667ab48..46b31aa8 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -69,17 +69,13 @@ explicitly (existing characteristics are still covered by field inheritance from the previously generated files, but review the report for entries missing ValidValues). - -The old version of this script parsed ONLY the HomeKit Accessory Simulator plist; -it has been rewritten because that plist lags far behind (163 characteristics -vs 242+ in plain-metadata.config at the time of writing) and requires Xcode. """ import argparse import json +from pathlib import Path import plistlib import sys -from pathlib import Path # --------------------------------------------------------------------------- # Source locations @@ -251,8 +247,9 @@ def fix_valid_values(char_info): # --------------------------------------------------------------------------- -def generate_characteristics(meta_chars, units, hap_properties, sim_chars, - manual, existing_chars, name_overrides): +def generate_characteristics( + meta_chars, units, hap_properties, sim_chars, manual, existing_chars, name_overrides +): """Build the HAP-python characteristics catalog. Steps: @@ -278,9 +275,11 @@ def generate_characteristics(meta_chars, units, hap_properties, sim_chars, uuid_to_name.setdefault(prev.get("UUID", "").upper(), prev_name) for slug, info in meta_chars.items(): uuid = long_uuid(info["ShortUUID"]) - name = (name_overrides.get(slug) - or uuid_to_name.get(uuid.upper()) - or to_hap_name(info["DefaultDescription"])) + name = ( + name_overrides.get(slug) + or uuid_to_name.get(uuid.upper()) + or to_hap_name(info["DefaultDescription"]) + ) slug_to_final[slug] = name entry = { "Format": info["Format"], @@ -296,8 +295,11 @@ def generate_characteristics(meta_chars, units, hap_properties, sim_chars, # Simulator overlay: richer permission strings + constraints. sim = sim_chars.get(entry["UUID"].upper()) if sim: - perms = [SIMULATOR_PERMS_MAP[p] for p in sim.get("Permissions", []) - if p in SIMULATOR_PERMS_MAP] + perms = [ + SIMULATOR_PERMS_MAP[p] + for p in sim.get("Permissions", []) + if p in SIMULATOR_PERMS_MAP + ] constraints = sim.get("Constraints") or {} for sim_key, out_key in SIMULATOR_CONSTRAINTS_MAP.items(): if sim_key in constraints: @@ -321,8 +323,10 @@ def generate_characteristics(meta_chars, units, hap_properties, sim_chars, for name, entry in catalog.items(): sim = sim_chars.get(entry["UUID"].upper()) if sim and "ValidValues" in (sim.get("Constraints") or {}): - c = {"ValidValues": sim["Constraints"]["ValidValues"], - "Format": entry["Format"]} + c = { + "ValidValues": sim["Constraints"]["ValidValues"], + "Format": entry["Format"], + } fix_valid_values(c) entry["ValidValues"] = c["ValidValues"] else: @@ -351,8 +355,9 @@ def generate_characteristics(meta_chars, units, hap_properties, sim_chars, return catalog, slug_to_final -def generate_services(meta_services, characteristics, manual, name_overrides, - existing_services): +def generate_services( + meta_services, characteristics, manual, name_overrides, existing_services +): """Build the HAP-python services catalog from plain-metadata + manual files. plain-metadata references characteristics by slug; each slug is translated @@ -370,12 +375,14 @@ def generate_services(meta_services, characteristics, manual, name_overrides, for slug, info in meta_services.items(): uuid = long_uuid(info["ShortUUID"]) name = uuid_to_name.get(uuid.upper()) or to_hap_name(info["DefaultDescription"]) + # Resolve the characteristic slugs referenced by the service into # catalog names using the slug -> name table (name_overrides["_slug_to_name"] # plus any manual name overrides). def resolve(slug_c): - return (name_overrides.get(slug_c) - or name_overrides.get("_slug_to_name", {}).get(slug_c)) + return name_overrides.get(slug_c) or name_overrides.get( + "_slug_to_name", {} + ).get(slug_c) req_opt = info["Characteristics"] entry = { @@ -397,7 +404,10 @@ def resolve(slug_c): else: unknown.append(slug_c) if unknown: - print(f" ! service {name}: unknown characteristics {unknown}", file=sys.stderr) + print( + f" ! service {name}: unknown characteristics {unknown}", + file=sys.stderr, + ) # COMPATIBILITY INHERITANCE from the previously generated catalog: # pyhap's Service() auto-instantiates the REQUIRED characteristics at @@ -423,8 +433,11 @@ def resolve(slug_c): # Optional characteristics that were dropped entirely are re-added # (the allowed list only ever grows). for c in prev.get("OptionalCharacteristics", []): - if c in characteristics and c not in entry["RequiredCharacteristics"] \ - and c not in entry["OptionalCharacteristics"]: + if ( + c in characteristics + and c not in entry["RequiredCharacteristics"] + and c not in entry["OptionalCharacteristics"] + ): entry["OptionalCharacteristics"].append(c) catalog[name] = entry @@ -445,8 +458,10 @@ def diff_report(old, new, label): added = sorted(set(new) - set(old)) removed = sorted(set(old) - set(new)) changed = sorted(k for k in set(new) & set(old) if new[k] != old[k]) - print(f"{label}: {len(new)} entries ({len(added)} added, {len(removed)} removed, " - f"{len(changed)} changed)") + print( + f"{label}: {len(new)} entries ({len(added)} added, {len(removed)} removed, " + f"{len(changed)} changed)" + ) for k in added: print(f" + {k} ({new[k].get('UUID')})") for k in removed: @@ -456,46 +471,74 @@ def diff_report(old, new, label): def main(): parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) - parser.add_argument("--plain-metadata", type=Path, default=PLAIN_METADATA_PATH, - help="Path to plain-metadata.config") - parser.add_argument("--simulator-plist", type=Path, default=SIMULATOR_PLIST, - help="Path to the HomeKit Accessory Simulator default.metadata.plist") - parser.add_argument("--manual-overrides", type=Path, default=MANUAL_OVERRIDES_FILE, - help="Path to hap_manual_overrides.json") - parser.add_argument("--write", action="store_true", - help="Write the generated files (default is a dry-run report)") - parser.add_argument("--allow-missing-simulator", action="store_true", - help="Proceed without the HomeKit Accessory Simulator " - "plist (ValidValues for new characteristics will be " - "missing; permissions use the bitmask).") + parser.add_argument( + "--plain-metadata", + type=Path, + default=PLAIN_METADATA_PATH, + help="Path to plain-metadata.config", + ) + parser.add_argument( + "--simulator-plist", + type=Path, + default=SIMULATOR_PLIST, + help="Path to the HomeKit Accessory Simulator default.metadata.plist", + ) + parser.add_argument( + "--manual-overrides", + type=Path, + default=MANUAL_OVERRIDES_FILE, + help="Path to hap_manual_overrides.json", + ) + parser.add_argument( + "--write", + action="store_true", + help="Write the generated files (default is a dry-run report)", + ) + parser.add_argument( + "--allow-missing-simulator", + action="store_true", + help="Proceed without the HomeKit Accessory Simulator " + "plist (ValidValues for new characteristics will be " + "missing; permissions use the bitmask).", + ) args = parser.parse_args() if not args.plain_metadata.exists(): - sys.exit(f"Primary metadata not found at {args.plain_metadata}. " - "This script must run on macOS.") + sys.exit( + f"Primary metadata not found at {args.plain_metadata}. " + "This script must run on macOS." + ) manual = json.loads(args.manual_overrides.read_text()) - meta_chars, meta_services, _units, hap_properties = load_plain_metadata(args.plain_metadata) - print(f"plain-metadata.config: {len(meta_chars)} characteristics, " - f"{len(meta_services)} services") + meta_chars, meta_services, _units, hap_properties = load_plain_metadata( + args.plain_metadata + ) + print( + f"plain-metadata.config: {len(meta_chars)} characteristics, " + f"{len(meta_services)} services" + ) sim_chars = {} if args.simulator_plist.exists(): sim_chars = load_simulator_plist(args.simulator_plist) print(f"simulator plist: {len(sim_chars)} characteristics (overlay enabled)") elif args.allow_missing_simulator: - print("WARNING: simulator plist not found - proceeding without the " - "overlay (--allow-missing-simulator). ValidValues for newly added " - "characteristics will be missing; permissions use the coarser " - "bitmask.", file=sys.stderr) + print( + "WARNING: simulator plist not found - proceeding without the " + "overlay (--allow-missing-simulator). ValidValues for newly added " + "characteristics will be missing; permissions use the coarser " + "bitmask.", + file=sys.stderr, + ) else: sys.exit( f"Simulator plist not found at {args.simulator_plist}.\n" "Without it, ValidValues for newly added characteristics are missing " "and permissions fall back to the coarser bitmask.\n" "Install Xcode's HomeKit Accessory Simulator, or pass " - "--allow-missing-simulator to proceed explicitly.") + "--allow-missing-simulator to proceed explicitly." + ) existing_chars = json.loads(CHAR_OUT_FILE.read_text()) existing_services = json.loads(SERVICE_OUT_FILE.read_text()) @@ -515,11 +558,18 @@ def main(): name_overrides["_slug_to_name"] = slug_to_name new_chars, slug_to_final = generate_characteristics( - meta_chars, _units, hap_properties, sim_chars, manual, existing_chars, - name_overrides) + meta_chars, + _units, + hap_properties, + sim_chars, + manual, + existing_chars, + name_overrides, + ) name_overrides["_slug_to_name"].update(slug_to_final) new_services = generate_services( - meta_services, new_chars, manual, name_overrides, existing_services) + meta_services, new_chars, manual, name_overrides, existing_services + ) print() diff_report(existing_chars, new_chars, "characteristics.json") @@ -528,7 +578,9 @@ def main(): if args.write: CHAR_OUT_FILE.write_text(json.dumps(new_chars, indent=3, sort_keys=True) + "\n") - SERVICE_OUT_FILE.write_text(json.dumps(new_services, indent=3, sort_keys=True) + "\n") + SERVICE_OUT_FILE.write_text( + json.dumps(new_services, indent=3, sort_keys=True) + "\n" + ) print("\nWrote", CHAR_OUT_FILE, "and", SERVICE_OUT_FILE) else: print("\nDry run - nothing written. Re-run with --write to update the files.") From 3b1bbbff27e18f3b375a1c2dcea5fa01a53be500 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 11:51:13 +0200 Subject: [PATCH 4/7] gen_hap_types: simulator plist contributes ValidValues only The Additional Tools simulator plist turned out to work differently from the old Xcode-bundled one: its Permissions list holds auth flags (securedRead/securedWrite) and the read/write/notify strings live in Properties - so mapping Permissions to pr/pw/ev emptied every permission list, and the plist no longer carries numeric constraints either. Mirror HAP-NodeJS's generator: permissions and min/max/step come exclusively from plain-metadata.config; the simulator plist is used only for enumerated ValidValues ({value: state} reversed to {state: value}), falling back to inheritance from the previous catalog per characteristic. Also autodetect the simulator at both install locations (with and without the Xcode nested Applications folder). --- scripts/gen_hap_types.py | 121 +++++++++++++++------------------------ 1 file changed, 45 insertions(+), 76 deletions(-) diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index 46b31aa8..452e0c32 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -23,15 +23,18 @@ ShortUUID, DefaultDescription } - Units, Properties (bit position legend for the Properties bitmask), Base UUID. -2. HomeKit Accessory Simulator plists (OPTIONAL OVERLAY, needs Xcode tools) - Located inside the "HomeKit Accessory Simulator.app" (see SIMULATOR_PLIST below). - The simulator metadata lags behind plain-metadata.config, but it carries two - things plain-metadata encodes poorly or not at all: - - Permissions in string form ("read", "write", "cnotify", "timedWrite", - "writeResponse", "uncnotify", ...) - richer than the bitmask. - - ValidValues constraints for enumerated characteristics. - When the simulator is not installed the script still works: permissions fall - back to the Properties bitmask and ValidValues fall back to source 4 below. +2. HomeKit Accessory Simulator plist (OPTIONAL OVERLAY, needs Xcode's + "Additional Tools for Xcode" download) + Located inside the "HomeKit Accessory Simulator.app" (see SIMULATOR_PLIST + below). The simulator metadata lags far behind plain-metadata.config (125 + characteristics at the time of writing), so it is used ONLY for what + plain-metadata does not contain: the enumerated ValidValues constraints. + This mirrors HAP-NodeJS, whose generator likewise takes permissions and + min/max/step exclusively from plain-metadata.config. (Note: in recent + simulator plists the Permissions list holds auth flags like + "securedRead"/"securedWrite" while the read/write/notify strings live in + Properties - neither is used here.) When the simulator is not installed + the script still works: ValidValues fall back to source 4 below. 3. scripts/hap_manual_overrides.json (MANUAL OVERRIDES - the maintenance escape hatch) Apple's machine-readable metadata only covers definitions that have SHIPPED in @@ -86,11 +89,17 @@ "/System/Library/PrivateFrameworks/HomeKitDaemon.framework/Resources/plain-metadata.config" ) -# Optional overlay: only present when Xcode's HomeKit Accessory Simulator is installed. -SIMULATOR_PLIST = Path( - "/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app/" - "Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist" -) +# Optional overlay: only present when Apple's HomeKit Accessory Simulator (part +# of the "Additional Tools for Xcode" download) is installed. The app may be +# dropped either into /Applications or into Xcode's nested Applications folder. +SIMULATOR_PLIST_CANDIDATES = [ + Path("/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app" + "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist"), + Path("/Applications/HomeKit Accessory Simulator.app" + "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist"), +] +SIMULATOR_PLIST = next((p for p in SIMULATOR_PLIST_CANDIDATES if p.exists()), + SIMULATOR_PLIST_CANDIDATES[0]) REPO_ROOT = Path(__file__).resolve().parent.parent CHAR_OUT_FILE = REPO_ROOT / "pyhap/resources/characteristics.json" @@ -113,25 +122,6 @@ "writeResponse": "wr", } -# Simulator plist permission strings -> HAP-python permission strings. -# (Kept from the previous version of this script; "uncnotify" has no -# HAP-python representation and is skipped.) -SIMULATOR_PERMS_MAP = { - "read": "pr", - "write": "pw", - "cnotify": "ev", - "timedWrite": "tw", - "writeResponse": "wr", -} - -# Simulator plist constraint keys -> HAP-python characteristic field names. -SIMULATOR_CONSTRAINTS_MAP = { - "MaximumValue": "maxValue", - "MinimumValue": "minValue", - "StepValue": "minStep", - "MaxLength": "maxLen", -} - # plain-metadata.config char fields -> HAP-python characteristic field names. METADATA_FIELDS_MAP = { "MaxValue": "maxValue", @@ -226,22 +216,6 @@ def load_simulator_plist(path): return chars -def fix_valid_values(char_info): - """Valid values are given in a value: key format. Reverse them.""" - for char in char_info.values(): - if "ValidValues" not in char: - continue - valid_values = {} - for value, state in char["ValidValues"].items(): - if "int" in char["Format"] or char["Format"].startswith("uint"): - value = int(value) - elif "float" == char["Format"]: - value = float(value) - state = state.replace(" ", "") # To camel case. - valid_values[state] = value - char["ValidValues"] = valid_values - - # --------------------------------------------------------------------------- # Generation # --------------------------------------------------------------------------- @@ -292,18 +266,9 @@ def generate_characteristics( perms, admin_only = perms_from_bitmask(info["Properties"], hap_properties) - # Simulator overlay: richer permission strings + constraints. - sim = sim_chars.get(entry["UUID"].upper()) - if sim: - perms = [ - SIMULATOR_PERMS_MAP[p] - for p in sim.get("Permissions", []) - if p in SIMULATOR_PERMS_MAP - ] - constraints = sim.get("Constraints") or {} - for sim_key, out_key in SIMULATOR_CONSTRAINTS_MAP.items(): - if sim_key in constraints: - entry[out_key] = constraints[sim_key] + # NOTE: the simulator plist does NOT contribute permissions or numeric + # constraints - those come exclusively from plain-metadata.config, + # like in HAP-NodeJS. The plist only provides ValidValues (below). entry["Permissions"] = perms if admin_only: @@ -318,20 +283,23 @@ def generate_characteristics( entry[key] = value catalog[name] = entry - if sim_chars: - # Apply ValidValues conversion on the overlay entries. - for name, entry in catalog.items(): - sim = sim_chars.get(entry["UUID"].upper()) - if sim and "ValidValues" in (sim.get("Constraints") or {}): - c = { - "ValidValues": sim["Constraints"]["ValidValues"], - "Format": entry["Format"], - } - fix_valid_values(c) - entry["ValidValues"] = c["ValidValues"] - else: - # No simulator: inherit ValidValues from the previous catalog. - for name, entry in catalog.items(): + # ValidValues: from the simulator plist when present (Apple encodes them + # as {value: state}; reverse to the {state: value} form HAP-python uses). + # Characteristics the simulator does not know about inherit ValidValues + # from the previously generated catalog. + for name, entry in catalog.items(): + sim = sim_chars.get(entry["UUID"].upper()) + sim_vv = (sim.get("Constraints") or {}).get("ValidValues") if sim else None + if sim_vv is not None: + valid_values = {} + for value, state in sim_vv.items(): + if "int" in entry["Format"] or entry["Format"].startswith("uint"): + value = int(value) + elif entry["Format"] == "float": + value = float(value) + valid_values[state.replace(" ", "")] = value + entry["ValidValues"] = valid_values + else: prev = existing_chars.get(name) if prev and prev.get("UUID") == entry["UUID"] and "ValidValues" in prev: entry["ValidValues"] = prev["ValidValues"] @@ -481,7 +449,8 @@ def main(): "--simulator-plist", type=Path, default=SIMULATOR_PLIST, - help="Path to the HomeKit Accessory Simulator default.metadata.plist", + help="Path to the HomeKit Accessory Simulator default.metadata.plist " + "(autodetected from the usual install locations)", ) parser.add_argument( "--manual-overrides", From d689a5f8bf1c552fca482ed16e46308e73c2e836 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 11:53:21 +0200 Subject: [PATCH 5/7] Mirror HAP-NodeJS's corrections of Apple metadata quirks Diffing the regenerated catalog against HAP-NodeJS's generated definitions (V=889) surfaced deliberate fixes they carry in their CharacteristicOverriding config, which our catalog needs too: - WRITE_RESPONSE on the five control-point characteristics (AccessCodeControlPoint, NFCAccessControlPoint, TransitionControl, DataStreamHAPTransport, SetupDataStreamTransport) - uint8 format for HomeKitCameraActive/EventSnapshotsActive/ PeriodicSnapshotsActive - the HKSV 17.99 guide and HAP-NodeJS both define them as uint8 while plain-metadata.config says bool - CurrentTemperature minValue -270 (spec minimum; metadata says 0 which clamps negative readings to 0) - LockLastKnownAction values 9/10 from the newer HAP spec Also regenerated with the simulator plist overlay now active. --- pyhap/resources/characteristics.json | 25 +++-- scripts/hap_manual_overrides.json | 137 ++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index 4e9360ab..43ed7861 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -3,7 +3,8 @@ "Format": "tlv8", "Permissions": [ "pr", - "pw" + "pw", + "wr" ], "UUID": "00000262-0000-1000-8000-0026BB765291" }, @@ -657,7 +658,7 @@ "UUID": "00000011-0000-1000-8000-0026BB765291", "maxValue": 100, "minStep": 0.1, - "minValue": 0, + "minValue": -270, "unit": "celsius" }, "CurrentTiltAngle": { @@ -709,7 +710,8 @@ "Format": "tlv8", "Permissions": [ "pr", - "pw" + "pw", + "wr" ], "UUID": "00000138-0000-1000-8000-0026BB765291" }, @@ -747,7 +749,7 @@ "UUID": "0000023D-0000-1000-8000-0026BB765291" }, "EventSnapshotsActive": { - "Format": "bool", + "Format": "uint8", "Permissions": [ "pr", "pw", @@ -864,7 +866,7 @@ "UUID": "0000006F-0000-1000-8000-0026BB765291" }, "HomeKitCameraActive": { - "Format": "bool", + "Format": "uint8", "Permissions": [ "pr", "pw", @@ -1030,11 +1032,13 @@ ], "UUID": "0000001C-0000-1000-8000-0026BB765291", "ValidValues": { + "SecuredPhysically": 9, "SecuredPhysically,Exterior": 2, "SecuredPhysically,Interior": 0, "SecuredRemotely": 6, "SecuredbyAutoSecureTimeout": 8, "SecuredbyKeypad": 4, + "UnsecuredPhysically": 10, "UnsecuredPhysically,Exterior": 3, "UnsecuredPhysically,Interior": 1, "UnsecuredRemotely": 7, @@ -1222,7 +1226,8 @@ "Format": "tlv8", "Permissions": [ "pr", - "pw" + "pw", + "wr" ], "UUID": "00000264-0000-1000-8000-0026BB765291" }, @@ -1427,7 +1432,7 @@ "UUID": "000000E4-0000-1000-8000-0026BB765291" }, "PeriodicSnapshotsActive": { - "Format": "bool", + "Format": "uint8", "Permissions": [ "pr", "pw", @@ -1855,7 +1860,8 @@ "Format": "tlv8", "Permissions": [ "pr", - "pw" + "pw", + "wr" ], "UUID": "00000131-0000-1000-8000-0026BB765291" }, @@ -2541,7 +2547,8 @@ "Format": "tlv8", "Permissions": [ "pr", - "pw" + "pw", + "wr" ], "UUID": "00000143-0000-1000-8000-0026BB765291" }, diff --git a/scripts/hap_manual_overrides.json b/scripts/hap_manual_overrides.json index b0a40605..2e86af94 100644 --- a/scripts/hap_manual_overrides.json +++ b/scripts/hap_manual_overrides.json @@ -13,7 +13,22 @@ "(TargetSlatState, TargetAirQuality, TelevisionSpeaker). Delete them once", "Apple ships them again. NOTE: the generator never removes a required or", "optional characteristic from an existing service - demotions in Apple's", - "metadata are ignored by design; use serviceOverrides to remove one deliberately." + "metadata are ignored by design; use serviceOverrides to remove one deliberately.", + "", + "characteristicOverrides also carries deliberate corrections of Apple's", + "plain-metadata.config, mirroring HAP-NodeJS's CharacteristicOverriding:", + " - WRITE_RESPONSE on the control-point characteristics (HAP spec requires", + " write-response; the metadata does not encode it);", + " - uint8 format for HomeKitCameraActive/EventSnapshotsActive/", + " PeriodicSnapshotsActive (the HKSV 17.99 guide defines them as uint8;", + " the metadata wrongly says bool);", + " - CurrentTemperature minValue -270 (spec minimum; the metadata says 0,", + " which makes HAP-python clamp negative readings to 0);", + " - LockLastKnownAction values 9/10 from the newer HAP spec.", + "Deliberately NOT mirrored from HAP-NodeJS: write perm on", + "ConfiguredName/IsConfigured/DisplayOrder (HAP-python never had it),", + "read-only PairingFeatures (kept writable for compatibility), and", + "SiriInputType maxValue 0 (kept at the metadata value 1)." ], "characteristicAdditions": { "BufferActivityCommand": { @@ -283,6 +298,15 @@ "snr": "SignalToNoiseRatio" }, "characteristicOverrides": { + "AccessCodeControlPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000262-0000-1000-8000-0026BB765291" + }, "CameraOperatingModeIndicator": { "Format": "bool", "Permissions": [ @@ -293,6 +317,85 @@ ], "UUID": "0000021D-0000-1000-8000-0026BB765291" }, + "CurrentTemperature": { + "Format": "float", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00000011-0000-1000-8000-0026BB765291", + "maxValue": 100, + "minStep": 0.1, + "minValue": -270, + "unit": "celsius" + }, + "DataStreamHAPTransport": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000138-0000-1000-8000-0026BB765291" + }, + "EventSnapshotsActive": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00000223-0000-1000-8000-0026BB765291", + "ValidValues": { + "Disable": 0, + "Enable": 1 + } + }, + "HomeKitCameraActive": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "0000021B-0000-1000-8000-0026BB765291", + "ValidValues": { + "Disable": 0, + "Enable": 1 + } + }, + "LockLastKnownAction": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000001C-0000-1000-8000-0026BB765291", + "ValidValues": { + "SecuredPhysically": 9, + "SecuredPhysically,Exterior": 2, + "SecuredPhysically,Interior": 0, + "SecuredRemotely": 6, + "SecuredbyAutoSecureTimeout": 8, + "SecuredbyKeypad": 4, + "UnsecuredPhysically": 10, + "UnsecuredPhysically,Exterior": 3, + "UnsecuredPhysically,Interior": 1, + "UnsecuredRemotely": 7, + "UnsecuredbyKeypad": 5 + } + }, + "NFCAccessControlPoint": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000264-0000-1000-8000-0026BB765291" + }, "NightVision": { "Format": "bool", "Permissions": [ @@ -303,6 +406,29 @@ ], "UUID": "0000011B-0000-1000-8000-0026BB765291" }, + "PeriodicSnapshotsActive": { + "Format": "uint8", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00000225-0000-1000-8000-0026BB765291", + "ValidValues": { + "Disable": 0, + "Enable": 1 + } + }, + "SetupDataStreamTransport": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000131-0000-1000-8000-0026BB765291" + }, "ThirdPartyCameraActive": { "Format": "bool", "Permissions": [ @@ -310,6 +436,15 @@ "ev" ], "UUID": "0000021C-0000-1000-8000-0026BB765291" + }, + "TransitionControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000143-0000-1000-8000-0026BB765291" } }, "serviceAdditions": { From c1dae729851ed30153a6dd577b044de176cce7b6 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 12:05:28 +0200 Subject: [PATCH 6/7] gen_hap_types: refresh docstrings for the ValidValues-only overlay - the simulator overlay now contributes ValidValues only (permissions always come from the plain-metadata bitmask), so drop the stale 'coarser bitmask fallback' wording - field inheritance applies per characteristic, not only when the simulator is missing entirely - document the three stability rules (UUID-based naming, required/optional service inheritance, ValidValues suppressing min/max) and the metadata corrections carried in characteristicOverrides --- scripts/gen_hap_types.py | 47 +++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index 452e0c32..e6c42af7 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -47,17 +47,39 @@ The overrides file supports: characteristicNameOverrides slug -> HAP-python name (when the derived name is wrong) characteristicAdditions new characteristics, same schema as characteristics.json - characteristicOverrides full replacement of a generated characteristic entry + characteristicOverrides full replacement of a generated characteristic entry; + also carries deliberate corrections of Apple metadata + quirks (mirroring HAP-NodeJS's CharacteristicOverriding) serviceAdditions new services, same schema as services.json serviceOverrides full replacement of a generated service entry 4. Previously generated output (FIELD INHERITANCE) - When the simulator is unavailable, some fields (most importantly "ValidValues", - occasionally "maxLen"/"unit") cannot be derived from plain-metadata.config. To - avoid silently dropping them, fields present in the existing - characteristics.json / services.json entry (matched by UUID) are carried over + Some fields cannot be derived from plain-metadata.config at all (most + importantly "ValidValues" for characteristics the simulator plist does not + know about, occasionally "maxLen"/"unit"). To avoid silently dropping them, + such fields are carried over from the entry in the existing + characteristics.json / services.json (matched by name and verified by UUID) when the freshly generated entry is missing them. This keeps the catalog - stable across regenerations on machines without the simulator. + stable across regenerations even without the simulator. + +STABILITY RULES +=============== +Regenerating must never break existing HAP-python accessories, so the +following rules apply on top of the raw metadata: + +- NAMING: characteristics and services keep the name already used in the + committed catalog for a given UUID (Apple's descriptions drift over time, + e.g. "CarbonDioxideDetected" vs "Carbon dioxide Detected", "Slat" vs + "Slats"). New UUIDs get a name derived from the description. +- SERVICES: pyhap's Service() auto-instantiates the REQUIRED characteristics + at runtime, so a required->optional demotion in Apple's metadata would + silently strip characteristics from existing accessories. Required stays + Required, Optional lists only ever grow; deliberate removals must go + through serviceOverrides. +- VALID VALUES: when a characteristic has enumerated ValidValues, the numeric + min/max/step constraints are dropped. HAP-python clamps values to min/max + BEFORE checking ValidValues, so keeping both would turn invalid values into + silent clamping instead of a rejection. OUTPUT ====== @@ -66,12 +88,11 @@ actually rewrite them. Always review the diff before committing. The simulator overlay is REQUIRED by default: without it, ValidValues for newly -added Apple characteristics would be silently missing and permissions fall -back to the coarser bitmask. If the HomeKit Accessory Simulator is not -installed, the script errors out - pass --allow-missing-simulator to proceed -explicitly (existing characteristics are still covered by field inheritance -from the previously generated files, but review the report for entries -missing ValidValues). +added Apple characteristics would be silently missing. If the HomeKit Accessory +Simulator is not installed, the script errors out - pass --allow-missing-simulator +to proceed explicitly (existing characteristics are still covered by field +inheritance from the previously generated files, but review the report for +entries missing ValidValues). """ import argparse @@ -228,7 +249,7 @@ def generate_characteristics( Steps: 1. Convert every plain-metadata characteristic to catalog form. - 2. Overlay simulator Permissions/ValidValues/constraints where available. + 2. Overlay simulator ValidValues where available. 3. Inherit fields (ValidValues etc.) from the previously generated file. 4. Apply manual overrides and additions last (highest precedence). Returns (catalog, slug_to_final) where slug_to_final maps each Apple slug From 136aa30ccfeb9b870b344af110fc4466af6c15d4 Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Mon, 9 Nov 2026 12:11:05 +0200 Subject: [PATCH 7/7] Type-annotate gen_hap_types.py and bring it under ruff and ty Add full type annotations (catalog/entry/Apple-metadata aliases) to the generator and bring it into the lint scope: - tox.ini: ruff check/format now also cover scripts/gen_hap_types.py (scripts/pickle_to_state.py keeps its pre-existing violations and stays out of scope) - pyproject.toml: add "scripts" to [tool.ty.src] Fixes surfaced by the tools: import sorting, long docstring/signature lines, an overwritten loop variable in the ValidValues conversion, an unused loop variable, and the untyped "_slug_to_name" hack - the slug -> name table is now passed as a separate, properly typed parameter. Generator output is unchanged (dry run reports 0/0/0). --- pyproject.toml | 1 + scripts/gen_hap_types.py | 189 ++++++++++++++++++++++++--------------- tox.ini | 4 +- 3 files changed, 119 insertions(+), 75 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb0a477f..444b091e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,6 +141,7 @@ include = [ "pyhap", "tests", "benchmarks", + "scripts", ] [tool.ty.environment] diff --git a/scripts/gen_hap_types.py b/scripts/gen_hap_types.py index e6c42af7..907c17a4 100755 --- a/scripts/gen_hap_types.py +++ b/scripts/gen_hap_types.py @@ -6,8 +6,7 @@ This script keeps HAP-python's characteristic/service catalog in sync with Apple's official HomeKit definitions without hand-editing JSON files. It mirrors the -approach used by HAP-NodeJS (homebridge/HAP-NodeJS, `src/lib/definitions/ -generate-definitions.ts`). +approach used by HAP-NodeJS. Sources, in order of increasing precedence: @@ -17,9 +16,11 @@ This file ships WITH macOS and is updated with every OS release, so it does not require Xcode or the HomeKit Accessory Simulator to be installed. It is a plain binary plist and contains, under PlistDictionary -> HAP: - - Characteristics: "" -> { DefaultDescription, Format, Properties (bitmask), - ShortUUID, MinValue/MaxValue/StepValue, Units, ... } - - Services: "" -> { Characteristics: {Required: [...], Optional: [...]}, + - Characteristics: "" -> { DefaultDescription, Format, + Properties (bitmask), ShortUUID, + MinValue/MaxValue/StepValue, Units, ... } + - Services: "" -> { Characteristics: {Required and + Optional characteristic slug lists}, ShortUUID, DefaultDescription } - Units, Properties (bit position legend for the Properties bitmask), Base UUID. @@ -45,13 +46,16 @@ ONCE, here, instead of re-parsing the PDF guide every time. When Apple eventually ships them in plain-metadata.config, the manual entry can simply be deleted. The overrides file supports: - characteristicNameOverrides slug -> HAP-python name (when the derived name is wrong) - characteristicAdditions new characteristics, same schema as characteristics.json - characteristicOverrides full replacement of a generated characteristic entry; - also carries deliberate corrections of Apple metadata - quirks (mirroring HAP-NodeJS's CharacteristicOverriding) - serviceAdditions new services, same schema as services.json - serviceOverrides full replacement of a generated service entry + characteristicNameOverrides slug -> HAP-python name (when the + derived name is wrong) + characteristicAdditions new characteristics, same schema + as characteristics.json + characteristicOverrides full replacement of a generated entry; + also carries deliberate corrections of Apple + metadata quirks (mirroring HAP-NodeJS's + CharacteristicOverriding) + serviceAdditions new services, same schema as services.json + serviceOverrides full replacement of a generated entry 4. Previously generated output (FIELD INHERITANCE) Some fields cannot be derived from plain-metadata.config at all (most @@ -100,6 +104,7 @@ from pathlib import Path import plistlib import sys +from typing import Any # --------------------------------------------------------------------------- # Source locations @@ -114,13 +119,18 @@ # of the "Additional Tools for Xcode" download) is installed. The app may be # dropped either into /Applications or into Xcode's nested Applications folder. SIMULATOR_PLIST_CANDIDATES = [ - Path("/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app" - "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist"), - Path("/Applications/HomeKit Accessory Simulator.app" - "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist"), + Path( + "/Applications/Xcode.app/Contents/Applications/HomeKit Accessory Simulator.app" + "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist" + ), + Path( + "/Applications/HomeKit Accessory Simulator.app" + "/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist" + ), ] -SIMULATOR_PLIST = next((p for p in SIMULATOR_PLIST_CANDIDATES if p.exists()), - SIMULATOR_PLIST_CANDIDATES[0]) +SIMULATOR_PLIST = next( + (p for p in SIMULATOR_PLIST_CANDIDATES if p.exists()), SIMULATOR_PLIST_CANDIDATES[0] +) REPO_ROOT = Path(__file__).resolve().parent.parent CHAR_OUT_FILE = REPO_ROOT / "pyhap/resources/characteristics.json" @@ -152,12 +162,27 @@ "Units": "unit", } +# --------------------------------------------------------------------------- +# Types +# --------------------------------------------------------------------------- + +# A single entry of the generated characteristics.json / services.json. +CharEntry = dict[str, Any] +ServiceEntry = dict[str, Any] +# The generated catalogs, keyed by the HAP-python characteristic/service name. +CharCatalog = dict[str, CharEntry] +ServiceCatalog = dict[str, ServiceEntry] +# Raw Apple metadata structures, keyed by the Apple slug (plain-metadata.config) +# or by the full UUID (simulator plist). +AppleDefs = dict[str, dict[str, Any]] + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def to_hap_name(description): +def to_hap_name(description: str) -> str: """Convert a human-readable description to the HAP-python catalog name. Apple metadata uses e.g. "Current Ambient Light Level" or "PM2.5 Density", @@ -168,12 +193,14 @@ def to_hap_name(description): return description.replace(" ", "").replace("-", "") -def long_uuid(short_uuid, base_uuid="-0000-1000-8000-0026BB765291"): +def long_uuid(short_uuid: str, base_uuid: str = "-0000-1000-8000-0026BB765291") -> str: """Expand a short UUID (e.g. "000000B0") into the full HAP UUID form.""" return f"{short_uuid}{base_uuid}" -def perms_from_bitmask(properties, hap_properties): +def perms_from_bitmask( + properties: int, hap_properties: dict[str, dict[str, Any]] +) -> tuple[list[str], bool]: """Decode the plain-metadata Properties bitmask into HAP-python permissions. `properties` is the integer bitmask; `hap_properties` is the HAP.Properties @@ -182,7 +209,7 @@ def perms_from_bitmask(properties, hap_properties): "AdminOnlyAccess": ["write"] in the output, mirroring HAP-NodeJS; HAP-python does not enforce this, it is informational. """ - perms = [] + perms: list[str] = [] admin_only = False for name, info in hap_properties.items(): if not properties & (1 << info["Position"]): @@ -202,7 +229,9 @@ def perms_from_bitmask(properties, hap_properties): # --------------------------------------------------------------------------- -def load_plain_metadata(path): +def load_plain_metadata( + path: Path, +) -> tuple[AppleDefs, AppleDefs, dict[str, Any], dict[str, dict[str, Any]]]: """Parse plain-metadata.config and return (characteristics, services, legend). Returns raw Apple dicts keyed by slug: @@ -222,7 +251,7 @@ def load_plain_metadata(path): ) -def load_simulator_plist(path): +def load_simulator_plist(path: Path) -> AppleDefs: """Parse the HomeKit Accessory Simulator plist (optional overlay). Returns {UUID -> char entry} with Name, Format, Permissions, Constraints. @@ -231,7 +260,7 @@ def load_simulator_plist(path): """ with path.open("rb") as fp: data = plistlib.load(fp) - chars = {} + chars: AppleDefs = {} for char in data.get("Characteristics", []): chars[char["UUID"].upper()] = char return chars @@ -243,8 +272,13 @@ def load_simulator_plist(path): def generate_characteristics( - meta_chars, units, hap_properties, sim_chars, manual, existing_chars, name_overrides -): + meta_chars: AppleDefs, + hap_properties: dict[str, dict[str, Any]], + sim_chars: AppleDefs, + manual: dict[str, Any], + existing_chars: CharCatalog, + name_overrides: dict[str, str], +) -> tuple[CharCatalog, dict[str, str]]: """Build the HAP-python characteristics catalog. Steps: @@ -257,15 +291,15 @@ def generate_characteristics( definitions reference characteristics by slug, so this map is required to resolve them to names that actually exist in the catalog. """ - catalog = {} - slug_to_final = {} + catalog: CharCatalog = {} + slug_to_final: dict[str, str] = {} # Names: prefer the name already used in the committed catalog for a given # UUID so that regenerating never renames an existing characteristic # (Apple's descriptions drift over time, e.g. "CarbonDioxideDetected" vs # "Carbon dioxide Detected"). New UUIDs get a name derived from the # description, with any manual override taking precedence. - uuid_to_name = {} + uuid_to_name: dict[str, str] = {} for prev_name, prev in existing_chars.items(): uuid_to_name.setdefault(prev.get("UUID", "").upper(), prev_name) for slug, info in meta_chars.items(): @@ -276,22 +310,20 @@ def generate_characteristics( or to_hap_name(info["DefaultDescription"]) ) slug_to_final[slug] = name - entry = { + + # NOTE: the simulator plist does NOT contribute permissions or numeric + # constraints - those come exclusively from plain-metadata.config, + # like in HAP-NodeJS. The plist only provides ValidValues (below). + perms, admin_only = perms_from_bitmask(info["Properties"], hap_properties) + entry: CharEntry = { "Format": info["Format"], - "Permissions": None, # filled below - "UUID": long_uuid(info["ShortUUID"]), + "Permissions": perms, + "UUID": uuid, } for meta_key, out_key in METADATA_FIELDS_MAP.items(): if meta_key in info: entry[out_key] = info[meta_key] - perms, admin_only = perms_from_bitmask(info["Properties"], hap_properties) - - # NOTE: the simulator plist does NOT contribute permissions or numeric - # constraints - those come exclusively from plain-metadata.config, - # like in HAP-NodeJS. The plist only provides ValidValues (below). - - entry["Permissions"] = perms if admin_only: entry["AdminOnlyAccess"] = ["write"] @@ -312,12 +344,15 @@ def generate_characteristics( sim = sim_chars.get(entry["UUID"].upper()) sim_vv = (sim.get("Constraints") or {}).get("ValidValues") if sim else None if sim_vv is not None: - valid_values = {} - for value, state in sim_vv.items(): + valid_values: dict[str, int | float] = {} + for raw_value, state in sim_vv.items(): + value: int | float if "int" in entry["Format"] or entry["Format"].startswith("uint"): - value = int(value) + value = int(raw_value) elif entry["Format"] == "float": - value = float(value) + value = float(raw_value) + else: + value = raw_value # non-numeric: keep the plist value as is valid_values[state.replace(" ", "")] = value entry["ValidValues"] = valid_values else: @@ -345,41 +380,45 @@ def generate_characteristics( def generate_services( - meta_services, characteristics, manual, name_overrides, existing_services -): + meta_services: AppleDefs, + characteristics: CharCatalog, + manual: dict[str, Any], + name_overrides: dict[str, str], + slug_to_name: dict[str, str], + existing_services: ServiceCatalog, +) -> ServiceCatalog: """Build the HAP-python services catalog from plain-metadata + manual files. plain-metadata references characteristics by slug; each slug is translated to the catalog name computed in generate_characteristics(). Slugs that are unknown (dropped from Apple metadata) are reported and skipped. """ - catalog = {} + catalog: ServiceCatalog = {} + + # Resolve the characteristic slugs referenced by the services into + # catalog names: manual name overrides first, then the slug -> name map + # built during generation (with a description-derived fallback for legacy + # slugs Apple no longer ships as characteristic entries). + def resolve(slug_c: str) -> str | None: + return name_overrides.get(slug_c) or slug_to_name.get(slug_c) # Same UUID-based naming rule as for characteristics: services keep the # name already used in the committed catalog (e.g. "Slat" instead of # Apple's newer "Slats" description for UUID 0x000000B9). - uuid_to_name = {} + uuid_to_name: dict[str, str] = {} for prev_name, prev in existing_services.items(): uuid_to_name.setdefault(prev.get("UUID", "").upper(), prev_name) - for slug, info in meta_services.items(): + for _slug, info in meta_services.items(): uuid = long_uuid(info["ShortUUID"]) name = uuid_to_name.get(uuid.upper()) or to_hap_name(info["DefaultDescription"]) - # Resolve the characteristic slugs referenced by the service into - # catalog names using the slug -> name table (name_overrides["_slug_to_name"] - # plus any manual name overrides). - def resolve(slug_c): - return name_overrides.get(slug_c) or name_overrides.get( - "_slug_to_name", {} - ).get(slug_c) - req_opt = info["Characteristics"] entry = { "OptionalCharacteristics": [], "RequiredCharacteristics": [], "UUID": long_uuid(info["ShortUUID"]), } - unknown = [] + unknown: list[str] = [] for slug_c in req_opt.get("Optional", []): cname = resolve(slug_c) if cname and cname in characteristics: @@ -443,7 +482,9 @@ def resolve(slug_c): # --------------------------------------------------------------------------- -def diff_report(old, new, label): +def diff_report( + old: dict[str, CharEntry], new: dict[str, CharEntry], label: str +) -> tuple[list[str], list[str], list[str]]: added = sorted(set(new) - set(old)) removed = sorted(set(old) - set(new)) changed = sorted(k for k in set(new) & set(old) if new[k] != old[k]) @@ -458,7 +499,7 @@ def diff_report(old, new, label): return added, removed, changed -def main(): +def main() -> None: parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) parser.add_argument( "--plain-metadata", @@ -471,7 +512,7 @@ def main(): type=Path, default=SIMULATOR_PLIST, help="Path to the HomeKit Accessory Simulator default.metadata.plist " - "(autodetected from the usual install locations)", + "(autodetected from the usual install locations)", ) parser.add_argument( "--manual-overrides", @@ -509,7 +550,7 @@ def main(): f"{len(meta_services)} services" ) - sim_chars = {} + sim_chars: AppleDefs = {} if args.simulator_plist.exists(): sim_chars = load_simulator_plist(args.simulator_plist) print(f"simulator plist: {len(sim_chars)} characteristics (overlay enabled)") @@ -534,31 +575,33 @@ def main(): existing_services = json.loads(SERVICE_OUT_FILE.read_text()) # slug -> HAP-python name, used to resolve the characteristic references - # inside service definitions. - # Base slug -> name table from descriptions. Used for legacy slugs that - # Apple still references in service definitions but no longer ships as - # characteristic entries (e.g. "on" when the UUID 0x25 entry is now - # described as "PowerState"). The FINAL names recorded during generation - # (below) take precedence over this description-derived fallback. + # inside service definitions. Base table derived from descriptions, used + # for legacy slugs that Apple still references in service definitions but + # no longer ships as characteristic entries (e.g. "on" when the UUID 0x25 + # entry is now described as "PowerState"). The FINAL names recorded during + # generation (below) take precedence over this description-derived fallback. slug_to_name = { slug: to_hap_name(info["DefaultDescription"]) for slug, info in meta_chars.items() } - name_overrides = dict(manual.get("characteristicNameOverrides", {})) - name_overrides["_slug_to_name"] = slug_to_name + name_overrides: dict[str, str] = dict(manual.get("characteristicNameOverrides", {})) new_chars, slug_to_final = generate_characteristics( meta_chars, - _units, hap_properties, sim_chars, manual, existing_chars, name_overrides, ) - name_overrides["_slug_to_name"].update(slug_to_final) + slug_to_name.update(slug_to_final) new_services = generate_services( - meta_services, new_chars, manual, name_overrides, existing_services + meta_services, + new_chars, + manual, + name_overrides, + slug_to_name, + existing_services, ) print() diff --git a/tox.ini b/tox.ini index 18652f89..cabd6d4a 100644 --- a/tox.ini +++ b/tox.ini @@ -53,8 +53,8 @@ package = skip deps = ruff commands = - ruff check pyhap tests benchmarks - ruff format --check pyhap tests benchmarks + ruff check pyhap tests benchmarks scripts/gen_hap_types.py + ruff format --check pyhap tests benchmarks scripts/gen_hap_types.py [testenv:ty] extras =