From 2e12ebcf9a98e5d2c5ae74b10ae0b45d786d396d Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 8 Nov 2026 15:48:29 +0100 Subject: [PATCH 1/2] Add HKSV 17.99 camera services and characteristics to the catalog Add the eight services and 27 characteristics of Apple's HomeKit Secure Video Open Source Compatibility Guide (Developer Preview 1.0) to the loader resources. Two of them, Contributing Sensors and Motion Enabled, also become optional characteristics of the Motion Sensor service. Timed write is advertised where the guide requires it. Writes to Motion Enabled and Streaming Enabled are marked admin only with an "AdminOnlyAccess": ["write"] catalog property. It is not part of the HAP representation and not enforced yet. Camera Recording Management keeps the characteristics classic recording relies on. --- pyhap/resources/characteristics.json | 235 +++++++++++++++++++++++++++ pyhap/resources/services.json | 94 ++++++++++- tests/test_loader.py | 166 ++++++++++++++++++- 3 files changed, 493 insertions(+), 2 deletions(-) diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index 596e5898..f185818d 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -133,6 +133,88 @@ "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" + }, + "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": [ @@ -142,6 +224,22 @@ ], "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": [ @@ -265,6 +363,14 @@ "ContactNotDetected": 1 } }, + "ContributingSensors": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008086-0000-1000-8000-0026BB765291" + }, "CoolingThresholdTemperature": { "Format": "float", "Permissions": [ @@ -825,6 +931,19 @@ ], "UUID": "00000022-0000-1000-8000-0026BB765291" }, + "MotionEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008087-0000-1000-8000-0026BB765291" + }, "Mute": { "Format": "bool", "Permissions": [ @@ -1055,6 +1174,15 @@ "SinglePress": 0 } }, + "RTPStreamingControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00008045-0000-1000-8000-0026BB765291" + }, "RecordingAudioActive": { "Format": "uint8", "Permissions": [ @@ -1237,6 +1365,13 @@ ], "UUID": "00000117-0000-1000-8000-0026BB765291" }, + "SensorUUID": { + "Format": "data", + "Permissions": [ + "pr" + ], + "UUID": "0000805B-0000-1000-8000-0026BB765291" + }, "SerialNumber": { "Format": "string", "MaximumLength": 64, @@ -1386,6 +1521,19 @@ "Tampered": 1 } }, + "StreamingEnabled": { + "AdminOnlyAccess": [ + "write" + ], + "Format": "bool", + "Permissions": [ + "pr", + "pw", + "ev", + "tw" + ], + "UUID": "00008041-0000-1000-8000-0026BB765291" + }, "StreamingStatus": { "Format": "tlv8", "Permissions": [ @@ -1420,6 +1568,14 @@ ], "UUID": "00000115-0000-1000-8000-0026BB765291" }, + "SupportedAudioStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008044-0000-1000-8000-0026BB765291" + }, "SupportedCameraRecordingConfiguration": { "Format": "tlv8", "Permissions": [ @@ -1464,6 +1620,14 @@ ], "UUID": "00000144-0000-1000-8000-0026BB765291" }, + "SupportedVideoStreamTiers": { + "Format": "tlv8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "00008043-0000-1000-8000-0026BB765291" + }, "SwingMode": { "Format": "uint8", "Permissions": [ @@ -1804,6 +1968,77 @@ "minValue": 0, "unit": "percentage" }, + "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" + }, "HardwareFinish": { "Format": "tlv8", "Permissions": [ diff --git a/pyhap/resources/services.json b/pyhap/resources/services.json index 98c95aa4..8b3b4e97 100644 --- a/pyhap/resources/services.json +++ b/pyhap/resources/services.json @@ -61,6 +61,80 @@ ], "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", @@ -103,6 +177,22 @@ ], "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": [ "StatusActive", @@ -400,7 +490,9 @@ "StatusFault", "StatusTampered", "StatusLowBattery", - "Name" + "Name", + "ContributingSensors", + "MotionEnabled" ], "RequiredCharacteristics": [ "MotionDetected" diff --git a/tests/test_loader.py b/tests/test_loader.py index afe640a1..8714f65c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,9 +1,18 @@ """Tests for pyhap.loader.""" +from unittest.mock import patch +from uuid import UUID + import pytest from pyhap import CHARACTERISTICS_FILE, SERVICES_FILE -from pyhap.characteristic import Characteristic +from pyhap.characteristic import ( + PROP_FORMAT, + PROP_MAX_VALUE, + PROP_MIN_VALUE, + PROP_PERMISSIONS, + Characteristic, +) from pyhap.loader import Loader, get_loader from pyhap.service import Service @@ -74,3 +83,158 @@ def test_get_loader() -> None: assert loader.serv_types == loader2.serv_types assert get_loader() == loader + + +# Accesses only admin controllers may use, not part of the HAP representation. +ADMIN_ONLY_ACCESS = "AdminOnlyAccess" + +# Camera services and characteristics of the HomeKit Secure Video 17.99 guide: +# name -> (short UUID, format, permissions, AdminOnlyAccess). +HKSV3_CHARS = { + "SensorUUID": ("805B", "data", {"pr"}, None), + "MotionEnabled": ("8087", "bool", {"pr", "pw", "ev", "tw"}, ["write"]), + "SupportedVideoStreamTiers": ("8043", "tlv8", {"pr", "ev"}, None), + "SupportedAudioStreamTiers": ("8044", "tlv8", {"pr", "ev"}, None), + "CameraCapabilities": ("8011", "tlv8", {"pr"}, None), + "ContributingSensors": ("8086", "tlv8", {"pr", "ev"}, None), + "CameraKey": ("8051", "tlv8", {"pw", "tw"}, None), + "CameraKeyID": ("8052", "tlv8", {"pr", "ev"}, None), + "BufferUploadCommand": ("8013", "tlv8", {"pr", "pw", "wr"}, None), + "BufferActivityCommand": ("8017", "tlv8", {"pw"}, None), + "BufferEventCommand": ("8014", "tlv8", {"pr", "pw", "wr"}, None), + "BufferEventSequenceNumber": ("8015", "uint32", {"pr", "ev"}, None), + "CameraRecordingPublishingPoint": ("8016", "tlv8", {"pr", "pw"}, None), + "CameraZones": ("8022", "tlv8", {"pr", "pw"}, None), + "StreamingEnabled": ("8041", "bool", {"pr", "pw", "ev", "tw"}, ["write"]), + "RTPStreamingControl": ("8045", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCSolicitOffer": ("8053", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCProvideAnswer": ("8054", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCStreamingControl": ("8056", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCNumberOfActiveSessions": ("8057", "uint8", {"pr", "ev"}, None), + "WebRTCReoffer": ("8058", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCUpdateSession": ("805C", "tlv8", {"pr", "pw", "wr"}, None), + "WebRTCSupportedVideoStreamTiers": ("8059", "tlv8", {"pr", "ev"}, None), + "WebRTCSupportedAudioStreamTiers": ("805A", "tlv8", {"pr", "ev"}, None), + "CameraClientCSR": ("8081", "tlv8", {"pr", "pw", "wr"}, None), + "CameraClientCertificate": ("8082", "tlv8", {"pr", "pw", "tw"}, None), + "CameraClientCertificateStatus": ("8083", "tlv8", {"pr", "ev"}, None), +} + +# name -> (short UUID, required, optional) +HKSV3_SERVICES = { + "CameraCapabilities": ( + "8010", + ["Version", "CameraCapabilities"], + ["ManuallyDisabled"], + ), + "CameraGlobalOperatingMode": ( + "8032", + ["HomeKitCameraActive", "StreamingEnabled", "CameraOperatingModeIndicator"], + ["ManuallyDisabled", "NightVision", "ThirdPartyCameraActive"], + ), + "CameraMotionZones": ("8021", ["Version", "Active", "CameraZones"], []), + "CameraBufferManagement": ( + "8000", + [ + "BufferUploadCommand", + "BufferActivityCommand", + "BufferEventCommand", + "BufferEventSequenceNumber", + "CameraRecordingPublishingPoint", + ], + [], + ), + "CameraMultiTierRTPStreamManagement": ( + "8031", + [ + "StreamingEnabled", + "StatusActive", + "SupportedVideoStreamTiers", + "SupportedAudioStreamTiers", + "SupportedRTPConfiguration", + "SetupEndpoints", + "RTPStreamingControl", + "SensorUUID", + ], + [], + ), + "CameraWebRTCStreamManagement": ( + "8033", + [ + "WebRTCSolicitOffer", + "WebRTCProvideAnswer", + "WebRTCStreamingControl", + "WebRTCNumberOfActiveSessions", + "WebRTCReoffer", + "WebRTCUpdateSession", + "WebRTCSupportedVideoStreamTiers", + "WebRTCSupportedAudioStreamTiers", + "StreamingEnabled", + "SensorUUID", + ], + [], + ), + "CameraKeyManagement": ("8050", ["CameraKey", "CameraKeyID"], []), + "CameraClientCertificateManagement": ( + "8080", + [ + "CameraClientCSR", + "CameraClientCertificate", + "CameraClientCertificateStatus", + ], + [], + ), +} + + +def _hap_uuid(short: str) -> UUID: + return UUID(f"0000{short}-0000-1000-8000-0026BB765291") + + +@pytest.mark.parametrize("name", HKSV3_CHARS) +def test_loader_hksv3_char(name: str) -> None: + """Test the HKSV 17.99 characteristics match the guide.""" + short, char_format, permissions, admin_only_access = HKSV3_CHARS[name] + char = Loader().get_char(name) + assert char.type_id == _hap_uuid(short) + assert char.properties[PROP_FORMAT] == char_format + assert set(char.properties[PROP_PERMISSIONS]) == permissions + assert char.properties.get(ADMIN_ONLY_ACCESS) == admin_only_access + + +def test_loader_hksv3_admin_only_not_advertised() -> None: + """Test the admin only marker stays out of the HAP representation.""" + char = Loader().get_char("StreamingEnabled") + with patch.object(char, "broker") as mock_broker: + mock_broker.iid_manager.get_iid.return_value = 2 + hap_repr = char.to_HAP() + assert hap_repr["perms"] == ["pr", "pw", "ev", "tw"] + assert ADMIN_ONLY_ACCESS not in hap_repr + + +def test_loader_hksv3_active_sessions_range() -> None: + """Test the WebRTC session count carries the guide's value range.""" + char = Loader().get_char("WebRTCNumberOfActiveSessions") + assert char.properties[PROP_MIN_VALUE] == 0 + assert char.properties[PROP_MAX_VALUE] == 255 + + +@pytest.mark.parametrize("name", HKSV3_SERVICES) +def test_loader_hksv3_service(name: str) -> None: + """Test the HKSV 17.99 services match the guide.""" + short, required, optional = HKSV3_SERVICES[name] + loader = Loader() + service = loader.get_service(name) + assert service.type_id == _hap_uuid(short) + assert [char.display_name for char in service.characteristics] == required + assert loader.serv_types[name]["OptionalCharacteristics"] == optional + assert set(optional) <= set(loader.char_types) + + +def test_loader_motion_sensor_hksv3_optional() -> None: + """Test the motion sensor gains the HKSV 17.99 chars only as optional.""" + loader = Loader() + optional = loader.serv_types["MotionSensor"]["OptionalCharacteristics"] + assert {"ContributingSensors", "MotionEnabled"} <= set(optional) + service = loader.get_service("MotionSensor") + assert [char.display_name for char in service.characteristics] == ["MotionDetected"] From 207041ce2e7017ff31e3a965a09578e0ae929ce7 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 8 Nov 2026 15:48:49 +0100 Subject: [PATCH 2/2] Fix the camera operating mode characteristic permissions Third Party Camera Active is state the camera reports, so controllers read and subscribe to it but do not write it. Night Vision and Camera Operating Mode Indicator are written with timed writes. The Camera Global Operating Mode service of HKSV 17.99 uses all three. This matches the definitions in HAP-NodeJS. --- pyhap/resources/characteristics.json | 7 ++++--- tests/test_loader.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index f185818d..639b04ff 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -220,7 +220,8 @@ "Permissions": [ "pr", "pw", - "ev" + "ev", + "tw" ], "UUID": "0000021D-0000-1000-8000-0026BB765291" }, @@ -965,7 +966,8 @@ "Permissions": [ "pr", "pw", - "ev" + "ev", + "tw" ], "UUID": "0000011B-0000-1000-8000-0026BB765291" }, @@ -1872,7 +1874,6 @@ "Format": "bool", "Permissions": [ "pr", - "pw", "ev" ], "UUID": "0000021C-0000-1000-8000-0026BB765291" diff --git a/tests/test_loader.py b/tests/test_loader.py index 8714f65c..a38f1426 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -238,3 +238,19 @@ def test_loader_motion_sensor_hksv3_optional() -> None: assert {"ContributingSensors", "MotionEnabled"} <= set(optional) service = loader.get_service("MotionSensor") assert [char.display_name for char in service.characteristics] == ["MotionDetected"] + + +@pytest.mark.parametrize( + ("name", "permissions"), + [ + ("CameraOperatingModeIndicator", {"pr", "pw", "ev", "tw"}), + ("NightVision", {"pr", "pw", "ev", "tw"}), + ("ThirdPartyCameraActive", {"pr", "ev"}), + ], +) +def test_loader_camera_operating_mode_permissions( + name: str, permissions: set[str] +) -> None: + """Test the operating mode settings shared with the HKSV 17.99 services.""" + char = Loader().get_char(name) + assert set(char.properties[PROP_PERMISSIONS]) == permissions