Repository navigation
Add HKSV 17.99 camera capabilities, motion zones and global operating mode - #532
Conversation
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.
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.
Add pyhap.hksv3 with the camera capabilities, contributing sensors and camera zones values of the HomeKit Secure Video Open Source Compatibility Guide. Lists use the HAP TLV8 list encoding: one TLV per item with an empty type 0 TLV between items of the same type. Values over 255 bytes are split into 255-byte fragments, and the decoder joins a TLV to the one before it only after such a full fragment, like hksv_recording and HAP-NodeJS. Enum values a controller sends that are not known yet are kept as ints instead of failing the write, and a zone without method or a polygon without identifier is read like HAP-NodeJS does. The tests check the encoder output and the decoder against hand written vectors and a small independent parser of the list encoding.
A new "sensors" camera option takes the image sensors of the camera, each with its real dimensions, type, intent (main or package camera) and video encodings. With it the camera advertises them in the Camera Capabilities service, accepts motion zones through the Camera Motion Zones service and adds Contributing Sensors and Motion Enabled to the motion sensor. Cameras without the option are unchanged. set_motion_detected reports the sensors that saw the motion and reports none while the user turned motion detection off. Integrations read the zones from camera_zones and override motion_zones_changed and motion_enabled_changed to react. The hooks only hear about changes, not about a controller writing the same value again. Zones, their Active state and Motion Enabled are kept in the driver's accessory state like the classic recording state: restored when the camera is rebuilt and returned to their defaults when the last pairing is removed, calling the hooks for the values that change once all state is reset. Both kinds of state share one save per burst of writes. A zones write that does not decode fails and leaves the previous zones in place. A warning is logged for sensors with fewer than the three video encodings the guide requires and for motion reported for sensors that are not configured.
A new "global_operating_mode" camera option adds the Camera Global Operating Mode service with the listed optional characteristics, after all other services so their instance IDs stay the same. The settings a controller writes are persisted and reset for a new home; changes from writes and resets call global_operating_mode_changed with the setting and its value. Manually Disabled and Third Party Camera Active are device state, set through set_manually_disabled and set_third_party_camera_active. streaming_allowed combines HomeKit Camera Active, Streaming Enabled and Manually Disabled. While it is False, the camera answers SetupEndpoints with an error and does not start streams. Cameras without the option are unchanged. When the service exists, its HomeKit Camera Active alone turns the camera on or off. The value on the Camera Operating Mode service of classic recording is then ignored, so a value left there cannot keep the camera off. A change closes a running recording and calls homekit_camera_active_changed like before; writing the same value again calls no hook.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #532 +/- ##
==========================================
+ Coverage 90.38% 91.39% +1.00%
==========================================
Files 26 27 +1
Lines 3798 4170 +372
Branches 459 522 +63
==========================================
+ Hits 3433 3811 +378
+ Misses 258 252 -6
Partials 107 107
🚀 New features to boost your workflow:
|
# Conflicts: # pyhap/resources/characteristics.json
|
Thanks for regenerating the catalog in #539. I merged dev, so my catalog commits are gone from the diff. Everything this PR uses is in there with the same UUIDs, including the timed write and read-only permissions of the operating mode characteristics I had fixed by hand. |
With the global operating mode, the last write to either HomeKit Camera Active now decides and the other follows, so a hub that only knows the Camera Operating Mode service can still turn the camera off. Running live streams end once streaming is not allowed anymore: camera off, Streaming Enabled off or manually disabled. A global_operating_mode option that is not a list raises TypeError.
| if not isinstance(chars, (list, tuple)): | ||
| raise ValueError( | ||
| "global_operating_mode must be a list of characteristic names" | ||
| ) |
There was a problem hiding this comment.
Hm, the method type hints require list[str], but here we want list or tuple. Update the input type hint if tuples are allowed (or maybe use Iterable?).
Also, we generally do not assert the input is of the correct type, so I would drop this if entirely.
There was a problem hiding this comment.
Done, it takes a Sequence[str] now and the check is gone.
| "Not an optional Camera Global Operating Mode characteristic: " | ||
| + ", ".join(unknown) |
| objs = tlv.decode(value, from_base64=True) | ||
| session_id = UUID(bytes=objs[SETUP_TYPES["SESSION_ID"]]) | ||
| if not self.streaming_allowed: | ||
| logger.warning("[%s] Streaming is turned off, refusing it", session_id) |
There was a problem hiding this comment.
Agreed, it's info now.
| return value | ||
|
|
||
|
|
||
| def _to_uuid(data: bytes) -> UUID: |
There was a problem hiding this comment.
there is utils.hap_type_to_uuid, can you reuse it?
There was a problem hiding this comment.
hap_type_to_uuid takes a HAP type string, here the TLV carries the 16 raw bytes. I dropped _to_uuid and use UUID(bytes=...) directly, it raises ValueError for a wrong length by itself.
The option takes any sequence of names and is no longer type checked. The service keeps its chars by name, built once in setup. A refused stream while streaming is turned off logs at info. Zone polygon identifiers use UUID(bytes=...) directly, it raises ValueError for a wrong length by itself.
Builds on the catalog from #539. All opt-in, cameras without the new options are unchanged.
pyhap/hksv3.py: TLV8 codecs for Camera Capabilities, Contributing Sensors and Camera Zones. Same TLV layout as HAP-NodeJS PR 1132.sensorsoption: Camera Capabilities, Camera Motion Zones, Contributing Sensors and Motion Enabled. Withoutrecordingit adds its own Motion Sensor service. Zones and Motion Enabled are persisted like the recording state.global_operating_modeoption: Camera Global Operating Mode service. Together withrecordingthere are two HomeKit Camera Active characteristics. The last write to either turns the camera on or off and the other follows, so a hub that only knows the Camera Operating Mode service still works. While streaming is turned off (camera off, Streaming Enabled off or manually disabled), SetupEndpoints is answered with an error and running live streams end.New API:
pyhap.hksv3,camera_zones,motion_zones_active,motion_enabled,streaming_allowed,set_motion_detected(sensors=),set_manually_disabled,set_third_party_camera_active, hooksmotion_zones_changed,motion_enabled_changed,global_operating_mode_changed(name, value). The new hooks only fire on changes, the hub often writes the same values several times. Withglobal_operating_modethat also goes forhomekit_camera_active_changedon writes to the global characteristic. Classic writes call it every time as before.With the default IID manager, turning on
sensorsfor a paired camera withrecordingchanges the IIDs of the recording services.Tested with unit tests (3.10, 3.11) and paired with a Home hub: recording works as before, turning the camera off/on writes the global operating mode and the hooks get name and value, and the Home app does not ask for a stream while the camera is off. The hub did not touch the motion zones service, even after drawing a zone in the Home app.
References:
AI use: written with the help of an AI coding assistant (Opus 5.5). I reviewed and tested the changes myself (pytest, ruff, ty, live with current iOS/tvOS hubs).