Repository navigation
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.
terminate() raises ProcessLookupError on an exited process, which surfaced as an unretrieved task exception. Ignore it and still reap the process; same for kill().
GET /characteristics returned the stored value of every characteristic, including write-only ones such as Identify or a camera key. The HAP spec only allows reads of characteristics with the paired read (pr) permission and answers everything else with -70405. HAP-NodeJS does the same. The getter is not called for such characteristics. The /accessories database already omitted their value; this adds tests for it.
Any characteristic could be subscribed to, whether or not it has the notify (ev) permission. A controller could then receive the values that another controller wrote to a write-only characteristic. Per the HAP spec, (un)subscribing a characteristic without ev now answers -70406 and leaves the subscriptions unchanged, the same as HAP-NodeJS. A value in the same request entry is not written. Some tests subscribed to Version, which has no ev permission. They now use CurrentDoorState.
A value written to a characteristic without the pw permission still reached its setter. Answer it with -70404 like HAP-NodeJS instead, without calling the characteristic, service or accessory setters. A subscription in the same entry and the other characteristics of the request are still handled.
A write carrying a pid that was never prepared on this connection, was already used or has outlived its TTL was still applied, and its event subscriptions were changed. Only characteristics without a value got -70410. The HAP specification says such a request must be ignored and answered with -70410. Do that for every characteristic in the request, as HAP-NodeJS does. The TTL is now measured with the monotonic clock, so a wall clock jump, e.g. from NTP right after boot, can't expire a timed write.
No behaviour change. The next commit fills in the response after awaiting async setters, which needs the result building in the driver and the HTTP response filling in the handler to work on their own.
A write with "r": true got the setter's return value back for any characteristic. The HAP spec ties write responses to the write response (wr) permission, and HAP-NodeJS ignores the request for other characteristics. Do the same. When a write response is sent, it becomes the characteristic's value instead of the written one, which HAP-NodeJS does as well. The sent value is the stored one, after the usual format checks. A response the characteristic can't hold is still sent and the written value is kept. The write response tests are now parametrized and also cover a wr characteristic without paired read: neither the written value nor the response can be read back.
The HAP specification requires a plain write to a characteristic with
the timed write ("tw") permission to be refused with -70410. HAP-NodeJS
does this.
A Home hub has been seen writing HomeKit Camera Active without a timed
write, and the HomeKit Secure Video guide gives Streaming Enabled and
Motion Enabled tw as well. Other controllers may not support timed
writes at all. So the check is opt-in: with enforce_timed_writes=True
the driver refuses such a write, otherwise it applies it and only logs
at debug level.
The last fragment was sliced with data[-0:], which duplicated the whole value and produced undecodable output. Only emit a trailing fragment when bytes remain.
A setter that is a coroutine function was called but never awaited. With "r": true the coroutine was passed to the JSON encoder, which raised TypeError, so the whole write failed with a 500. Writes from a controller now wait for coroutine setters before they are answered, through the existing delayed response. Accessory and service setters still run first. A setter still running after the response timeout is cancelled and its characteristic answers -70408; the other characteristics keep their results. A setter that raises answers -70402, and a closed connection cancels the setters. The return value becomes the write response and the stored value under the same rules as for sync setters. The public set_characteristics() keeps its signature. Called directly, it schedules coroutine setters and answers at once. Only coroutines are awaited; a sync setter that returns a task or future works as before.
A write response of a characteristic that also has pr was stored as its value, so any paired controller could read it with GET /characteristics or /accessories. Keep it per connection instead: the connection that wrote reads its last response until it closes. Other connections read the characteristic's own value, which the write leaves as it was, also when the setter raises or returns nothing. Any new write of a connection drops its former response. Characteristics without pr keep the response as the value.
The writing connection reads an async setter's response back, other connections don't, the request stays private while the setter runs, and a response that completes as the connection closes is not kept.
# Conflicts: # pyhap/resources/characteristics.json
Python 3.10 reached its end of life in October 2026. asyncio.timeout replaces the async_timeout backport.
An accessory that keeps per-controller state, such as camera streams a controller set up, has no way to learn that the controller went away. The driver now calls Accessory.client_disconnected() from connection_lost, and a bridge passes it on to each bridged accessory. An accessory that raises is logged and does not stop the others or the rest of the connection cleanup.
The controller address of a Setup Endpoints write is now accepted only as the canonical IP of the announced version, without an IPv6 scope ID. Any other address, a missing address, port or SRTP field, a version or port field of the wrong length or an empty crypto suite answers with an error and sets up no session. set_endpoints() also returns that error answer. The default start_stream also builds its argument list by formatting each argument of the command template, instead of splitting the formatted command.
A controller writes Setup Endpoints and reads the answer back. The answer carries the session keys, so it now stays with the connection that wrote, like a write response. A characteristic with read_back_response keeps what its setter returns for the writing connection, until it writes again or closes. Other connections and /accessories read the characteristic's own value, and the written request is not left as that value either. The same now holds for any write to a readable write response characteristic, with or without r. set_endpoints() returns its answer instead of setting the value.
# Conflicts: # tests/test_camera.py
# Conflicts: # pyhap/accessory_driver.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py # pyhap/hksv3.py # tests/test_camera.py # tests/test_camera_operating_mode.py # tests/test_hksv3.py
# Conflicts: # pyhap/camera.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py
…connection # Conflicts: # pyhap/camera.py # tests/test_accessory_driver.py # tests/test_camera.py
# Conflicts: # pyhap/camera.py # tests/test_camera.py
A subclass that wraps set_endpoints, calls super() and returns nothing got no answer once set_endpoints only returned it. set_endpoints sets the answer, or a refusal, as the value again and returns it. The driver keeps what a read back setter leaves as the value for the writer only and puts the shared value back. Neither the former value of a refused write nor the request a setter left alone counts as an answer. An async setter must return the answer, a value it sets itself is shared. Correct the write response docstring for a write without r, type the Setup Endpoints helpers, document read_back_response and share the Setup Endpoints request of the camera tests.
# Conflicts: # tests/test_camera.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #547 (it uses the same worker and pool). Until it is merged, the diff against dev includes it and what it builds on.
pyhap/media/recording.pyandmpegts.py: aRecorderkeeps a ring buffer of the channel's last access units, aligned to IRAP pictures and bounded in size. On a recording it feeds the buffer and the live stream as MPEG-TS to a transcoder (ffmpeg) and returns its fragmented MP4. It runs in process or in the media worker.recording={"delegate": ...}: the camera arms the delegate while the hub has recording on and the camera is on, advertises itsprebuffer_msand records with it.pyhap.hds_recording.RecordingProduceris the protocol;handle_recording_streamstays the override for everything else.pyhap/media/recording_delegate.py:RecordingMediaDelegateleases the recording channel from the pool. With a prebuffer it keeps the channel pulled while armed, without one (the default) it only pulls for a recording, so an idle camera costs nothing. A slow hub ends the recording instead of growing a buffer.Observed: hubs do not upload HDS recordings of cameras that also offer multi-tier streams, so the clips never appear in the Home app. The camera logs a warning for that combination.
Tested with unit tests (fake transcoder, in process and in the worker) on 3.11 and 3.14, ruff, ty, and live with current iOS/tvOS hubs: clips of a classic H.264 camera with a 4 s prebuffer appear in the Home app and start before the trigger.
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).