Repository navigation
Enforce the pr, pw, ev and wr permissions - #535
Merged
Merged
Conversation
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 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.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #535 +/- ##
==========================================
+ Coverage 90.38% 90.45% +0.06%
==========================================
Files 26 26
Lines 3798 3844 +46
Branches 459 469 +10
==========================================
+ Hits 3433 3477 +44
- Misses 258 260 +2
Partials 107 107
🚀 New features to boost your workflow:
|
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.
ikalchev
requested changes
Nov 9, 2026
ikalchev
left a comment
Owner
There was a problem hiding this comment.
Awesome! Some minor comments and questions from my side.
Contributor
Author
|
Thanks for the review. All five points are addressed, and dev is merged in. |
This was referenced Nov 10, 2026
RaHehl
marked this pull request as ready for review
November 10, 2026 13:13
client_update_value takes notify=False for a write response request on a readable characteristic, so ev subscribers don't get a request that only the writer may read, e.g. a WiFi configuration with a key. HAPValue moves to pyhap.characteristic next to the formats.
RaHehl
added a commit
to RaHehl/HAP-python
that referenced
this pull request
Nov 10, 2026
ikalchev#535 adds client_addr as the second argument of get_characteristics, so a positional client_uuid could end up as the address once both are in. Also note why privileges are checked before availability.
RaHehl
added a commit
to RaHehl/HAP-python
that referenced
this pull request
Nov 10, 2026
A subclass that still sets the Setup Endpoints answer as the value made it readable for every controller. Whatever the setter of a read back characteristic leaves in the value now goes to the writer's store and the former value comes back. A plain write to another write response characteristic stays readable for everyone, as in ikalchev#535. The read back test now checks the answer's status instead of the session ID.
ikalchev
pushed a commit
that referenced
this pull request
Nov 10, 2026
* 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. * 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. * Enforce admin-only characteristic access The catalog can mark a characteristic with "AdminOnlyAccess" (Motion Enabled and Streaming Enabled list "write"), but nothing checked it. Pass the controller of the session from the HAP handler to the driver and refuse a write, or a read when the list contains "read", with -70401 unless the controller is a paired admin. A request without a known controller is refused too. This follows adminOnlyAccess in HAP-NodeJS. Characteristics without the property are not affected. * Pass client_uuid by keyword only #535 adds client_addr as the second argument of get_characteristics, so a positional client_uuid could end up as the address once both are in. Also note why privileges are checked before availability. * Keep only the admin-only test in the loader tests The catalog tests came with the old catalog commits and are not about admin-only access. * Enforce AdminOnlyAccess for writes only * Refuse an explicit null from a regular controller before any setter Also say in the catalog generator that the driver enforces admin-only writes.
ikalchev
added a commit
that referenced
this pull request
Nov 10, 2026
* Return -70405 when reading a characteristic without paired read
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.
* Answer -70406 for event subscriptions without notify permission
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.
* Return -70404 when writing a characteristic without paired write
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.
* Ignore timed writes with an unknown or expired pid
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.
* Split building the characteristics write response into helpers
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.
* Only send write responses for characteristics with wr permission
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.
* Check the timed write permission on writes
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.
* Await async characteristic setters before answering the write
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.
* Only let the writing connection read back a write response
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.
* Test write response privacy with async setters
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.
* Drop the redundant bool() around the write response check
* Type the write response helpers with the HAP JSON value types
* Explain why a private write puts the former value back
* Keep the Setup Endpoints answer per connection
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.
* Don't tell subscribers the request of a write with r
client_update_value takes notify=False for a write response request on
a readable characteristic, so ev subscribers don't get a request that
only the writer may read, e.g. a WiFi configuration with a key.
HAPValue moves to pyhap.characteristic next to the formats.
* Type the timed write pid and ignore a pid that is not an int
A pid of another type, e.g. a list, raised a TypeError instead of
answering -70410.
* Name the fields of a pending async write and keep write_response a bool
A write asks for a response only with "r": true.
* Keep read back answers private, also when the setter sets them
A subclass that still sets the Setup Endpoints answer as the value made
it readable for every controller. Whatever the setter of a read back
characteristic leaves in the value now goes to the writer's store and
the former value comes back. A plain write to another write response
characteristic stays readable for everyone, as in #535. The read back
test now checks the answer's status instead of the session ID.
* Run test_start_from_sync on the driver's own loop
It set a new default executor on the shared test loop and shut it down
when the driver stopped, so later tests on the same xdist worker failed
with "cannot schedule new futures after shutdown". The driver now makes
its own loop and executor, as a sync start does.
* Answer a Setup Endpoints refused while streaming is off only to the writer
* Keep setting the Setup Endpoints answer as the value
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.
* Use incomaptible-method-override instead of invalid-
ty was upgraded today
---------
Co-authored-by: Ivan Kalchev <25887324+ikalchev@users.noreply.github.com>
ikalchev
added a commit
that referenced
this pull request
Nov 10, 2026
* 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.
* 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.
* Add TLV8 codecs for the HKSV 17.99 camera values
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.
* Add HKSV 17.99 camera capabilities and motion zones
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.
* Add the HKSV 17.99 camera global operating mode
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.
* Return -70405 when reading a characteristic without paired read
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.
* Answer -70406 for event subscriptions without notify permission
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.
* Return -70404 when writing a characteristic without paired write
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.
* Ignore timed writes with an unknown or expired pid
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.
* Split building the characteristics write response into helpers
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.
* Only send write responses for characteristics with wr permission
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.
* Check the timed write permission on writes
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.
* Await async characteristic setters before answering the write
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.
* Only let the writing connection read back a write response
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.
* Test write response privacy with async setters
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.
* Drop the redundant bool() around the write response check
* Type the write response helpers with the HAP JSON value types
* Explain why a private write puts the former value back
* Tell accessories when a controller's connection closes
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.
* Accept only a canonical IP address in Setup Endpoints
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.
* Keep the Setup Endpoints answer per connection
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.
* Add TLV8 codecs for the HKSV 17.99 multi-tier RTP stream management
The Camera Multi-Tier RTP Stream Management service of the HomeKit Secure
Video 17.99 guide advertises video and audio stream tiers, sets up a
session with Setup Endpoints and starts and ends streams through RTP
Streaming Control.
Add the video and audio stream tiers and their Supported Video/Audio
Stream Tiers values, the decoder for RTP Streaming Control writes and the
encoder for its read back status, the Setup Endpoints request and answer,
and tiers_from_sensor() to offer the video encodings of a sensor as
tiers. Unknown commands are kept as plain ints, like the other enums of
the module.
Tiers are frozen. A video tier also carries the peak bit rate of its
encoding, which is not part of the TLV. The payload types 99 and 110 are
constants, so the RTP tiers, the WebRTC tiers and a WebRTC offer agree.
check_audio_tier() and check_video_tiers() hold the tiers against the
guide: 20 ms mono Opus, at least one video encoding, known qualities,
Highest only with High and a peak not below the average. A gap of more
than 10 % of the peak and peaks above the guide's bit rate table only
warn, the guide recommends them. A frame between two rows of the table
takes the larger one, and a frame above 4K warns.
The module docstring now says the API is experimental.
* Add TLV8 codecs for the HKSV 17.99 WebRTC stream management
The Camera WebRTC Stream Management service of the HomeKit Secure Video
17.99 guide (sections 3.7 and 4.17 to 4.22) negotiates a session through
write response characteristics: the accessory offers, the controller
answers, renegotiates, updates SFrame keys and ends the session.
Add the offer options, ICE candidates and SFrame key data, the requests
and responses of each characteristic and their status enums. Each type
encodes and decodes, so tests can play the controller.
WebRTCOffer and WebRTCReofferAnswer are what a WebRTC media backend
returns. With IceCandidate and SFrameKey they are the types the HAP side
and the media share, all frozen. The offer's SDP carries the ICE
candidates, so the Solicit Offer answer lists none, as in the Scrypted
HomeKit plugin. generate_sframe_key() makes a key to send with.
An answer or reoffer with an SDP above 32 KiB, a write with more than
64 ICE candidates or one above 1 KiB, and an Update Session with more
than 16 keys to add or to remove do not decode, so the media side never
sees one.
* Add the session state machine of the HKSV 17.99 stream services
The multi-tier RTP and WebRTC stream services of the HomeKit Secure
Video 17.99 guide keep sessions that a controller sets up and the camera
streams through a delegate. Both need the same lifecycle, so it lives in
one place, pyhap.camera_stream, which the services in the next changes
build on.
A session goes from new to prepared or solicited, through answering for
WebRTC, to started or active, and ends from any state. Only the
connection that set it up may control it, and no two sessions of a
controller share an ID. A controller has a limit of sessions in all and
per connection.
A delegate hook runs as a task the session owns. The HAP request waits
for it until one deadline, hook_timeout after the write, which must leave
a second of the 9 second HAP response timeout. A hook that is still
running then is cancelled and its session ends. Each session the
delegate saw is released exactly once, in the background and after its
cancelled hooks finished, or after hook_timeout if one ignores its
cancellation. So a timeout never waits for a second hook inside the
request. Timers re-check the state before they end a session.
A delegate fails a request with StreamError(status) and ends a session
on its own with session.end(reason). It keeps its state by session
object, since a new session may reuse the ID of one still being
released. The integrator ends sessions with end_session() and
end_all_sessions(). close() ends everything, waits for the background
tasks and takes no new session afterwards.
* Add the HKSV 17.99 multi-tier RTP and WebRTC stream services
A controller that streams through the Camera Multi-Tier RTP Stream
Management service of the HomeKit Secure Video 17.99 guide does not fall
back to the RTP stream management service for remote viewing, which
needs the Camera WebRTC Stream Management service. So both come together
or not at all.
The new multi_tier and webrtc options add a MultiTierController and a
WebRTCController per sensor, in the spirit of the SecureVideoController
of HAP-NodeJS #1132. Camera composes them. The options take a
MultiTierConfig and a WebRTCConfig, or dicts of their fields. Unknown
keys raise. Both need the sensors, global_operating_mode, recording,
video and audio options. iOS 27 shows a camera without the classic RTP
stream management service as not responding. The services link to each
other and to the data stream transport.
The multi-tier service offers the sensor's video encodings as H.265
tiers and exactly one Opus tier, with the payload types 99 and 110. The
WebRTC service offers the same tiers and picks the one a remote offer
advertises, by default the Medium one. The tiers are checked against
the guide. Status Active follows the global operating mode and the
service's Streaming Enabled, which survives a restart, and the sessions
of a service end when it may no longer stream.
Each service has at least the sessions the guide requires, five and six.
max_sessions_per_connection limits one connection: three by default for
multi-tier, and all of them for WebRTC, since remote viewers share the
home hub's connection.
The media comes from typed delegates, MultiTierDelegate and
WebRTCDelegate, which release a session in handle_end(). The sessions
follow in the next changes. Camera ends the sessions of a closed
connection, stops them with the camera and has end_session() and
end_all_sessions() for the integrator.
* Set up, start and end HKSV 17.99 multi-tier RTP sessions
A controller sets up a multi-tier session with Setup Endpoints and
starts, changes or ends its stream with RTP Streaming Control.
Setup Endpoints:
- Only the canonical address of the announced IP version is accepted,
and only AES_CM_128_HMAC_SHA1_80 keys. A write that does not decode,
and one while streaming is not allowed, without a delegate or for a
session of another connection, answers an error. Beyond max_sessions,
or max_sessions_per_connection for the writing connection, it is busy.
- The session is registered before the delegate prepares it, so a closed
connection or streaming turned off ends it too. A failed or slow
prepare answers an error and the delegate still releases the session.
- The answer has the accessory's address on the controller's
connection, in the controller's IP version, as in HAP-NodeJS #1132.
The delegate may change it. The driver's new local_address() finds it.
Only the writing connection reads the answer back.
- Setting a session up again releases the former one first. If that
release fails or overruns the deadline, streaming turned off, or
another write set the same ID up or took the room meanwhile, it
answers an error or busy.
RTP Streaming Control:
- Only the connection that set the session up controls it.
- A start picks advertised tiers and calls handle_start(session,
video_tier, audio_tier), the audio tier is None for video only. A
start of a started session calls handle_reconfigure(), which keeps the
SSRCs and SRTP keys of the running stream.
- An end releases the session once.
A session that is not started within setup_timeout (30 s) ends. A
started session ends after idle_timeout (30 s) unless the delegate calls
session.keep_alive(). idle_timeout=None turns that off.
open_rtcp_monitor() does that for the controller's RTCP, as
homebridge-camera-ffmpeg does: it binds a UDP port, announces it as the
accessory's video port and keeps the session alive for each datagram
from the controller's address. A former session with the same ID keeps
nothing alive.
* Stream HKSV 17.99 multi-tier sessions with ffmpeg
FFmpegMultiTierDelegate streams each session with its own ffmpeg
process. Pass it as the delegate of the multi_tier option, with the
input arguments, and audio=False for an input without audio.
The command is an argument list, ffmpeg, the input arguments and the
outputs, which no shell sees and nothing splits. It copies the video
and encodes 16 kHz mono Opus in 20 ms frames, which ffmpeg sends with
the 48 kHz RTP clock, with the session's SSRCs, keys and the payload
types 99 and 110. A start without an audio tier has no audio output.
A tier change keeps the process as it is, without adapting the stream:
a copied stream cannot change its tier, and a new process with the same
SSRC and SRTP key would repeat the keystream.
ffmpeg does not report the controller's RTCP. So the delegate opens an
RTCP monitor in prepare, announces its port as the video port and lets
ffmpeg send video from a free port, with its RTCP to the controller's
video port. The controller's RTCP keeps the session alive, without it the
session ends after idle_timeout. The session also ends when ffmpeg exits
on its own.
The SRTP keys are not logged, and URLs in the logged command and in
ffmpeg's output keep only their scheme, host and port.
* Solicit, answer and end HKSV 17.99 WebRTC sessions
A controller views remotely through the Camera WebRTC Stream Management
service: it solicits an offer, provides its answer and ends the session
with WebRTC Streaming Control (guide chapter 5).
- Solicit Offer: no offer while the camera is off (privacy mode), while
streaming is not allowed, without a delegate, beyond max_sessions or
beyond max_sessions_per_connection for the writing connection. The
delegate gets the session and the tiers the offer advertises. SFrame
is used unless the controller turned it off explicitly. The Scrypted
HomeKit plugin found remote viewing needs it when the option is left
out. The offer must carry an SFrame key exactly then. The SDP carries
the ICE candidates, the answer lists none.
- Provide Answer: only from the soliciting connection and only once. A
second answer while the first is applied is busy, one after it an
error. An answer above the SDP or candidate limit does not decode and
answers an error before the delegate sees it. Afterwards the session
counts in Number of Active Sessions.
- A session without an applied answer ends after answer_timeout (30 s).
The timer stops only once the answer applied and checks the state
again when it fires.
- A failed or slow offer or answer ends the session. The delegate's
handle_end() releases every session it saw exactly once, in the
background, so a timeout never waits for it inside the HAP response
time.
The answers are write responses. A test through the driver checks that
only the writing connection reads them back.
* Renegotiate HKSV 17.99 WebRTC sessions and update their keys
An active WebRTC session can be renegotiated with WebRTC Reoffer: the
controller offers, the delegate's handle_reoffer() answers, optionally
with a new SFrame key. WebRTC Update Session hands the delegate the
SFrame keys the controller adds and the key IDs it removes.
Both need an active session of the writing connection. A request while
another hook of the session runs answers busy. A failure answers its
status and keeps the session, a hook that takes too long ends it. A
reoffer above the SDP limit answers an error before the delegate sees
it.
* Keep reading ffmpeg's output in chunks so status lines can't block it
ffmpeg ends its status line with a carriage return. Reading stderr line by
line failed after 64 KiB without a newline, nobody drained the pipe any
more and ffmpeg blocked. The delegate now starts ffmpeg without banner and
status, reads its output in chunks and splits it on both line ends. A
failing reader no longer keeps the watcher from ending the session.
* Don't tell subscribers the request of a write with r
client_update_value takes notify=False for a write response request on
a readable characteristic, so ev subscribers don't get a request that
only the writer may read, e.g. a WiFi configuration with a key.
HAPValue moves to pyhap.characteristic next to the formats.
* Type the timed write pid and ignore a pid that is not an int
A pid of another type, e.g. a list, raised a TypeError instead of
answering -70410.
* Bound the SFrame keys and SDP m-line indexes controllers send
An SFrame key must have 16 to 64 bytes, and an m-line index decodes only
up to the 16 bits it encodes with.
* Tighten multi-tier and WebRTC session handling
- A start keeps the setup timer from firing and restores the controller
SSRCs if it fails.
- A reoffer answer may only carry an SFrame key for a session with SFrame.
- The ffmpeg delegate sends audio from a free port instead of the
controller's.
- Delegates are told how sessions stay alive: keep_alive for multi-tier,
session.end() on media failure for WebRTC.
- Client addresses are typed, the shared stream helpers are internal, and
a session of another connection is logged at debug level.
* Name the fields of a pending async write and keep write_response a bool
A write asks for a response only with "r": true.
* Keep read back answers private, also when the setter sets them
A subclass that still sets the Setup Endpoints answer as the value made
it readable for every controller. Whatever the setter of a read back
characteristic leaves in the value now goes to the writer's store and
the former value comes back. A plain write to another write response
characteristic stays readable for everyone, as in #535. The read back
test now checks the answer's status instead of the session ID.
* Test a closed connection without an accessory
* Normalize Setup Endpoints addresses and keep set_endpoints returning None
An IPv6 address in another notation, e.g. upper case or without ::, is
accepted in its canonical form instead of being refused. Only a scope ID,
the wrong IP version or an invalid address is refused. set_endpoints
sets the refusal as the value and returns nothing, as before.
* Run test_start_from_sync on the driver's own loop
It set a new default executor on the shared test loop and shut it down
when the driver stopped, so later tests on the same xdist worker failed
with "cannot schedule new futures after shutdown". The driver now makes
its own loop and executor, as a sync start does.
* Answer a Setup Endpoints refused while streaming is off only to the writer
* Test that a refused Setup Endpoints reaches only the writer
* Decode the stream service UUIDs with UUID(bytes=...) like the zones
* Keep setting the Setup Endpoints answer as the value
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.
---------
Co-authored-by: Ivan Kalchev <25887324+ikalchev@users.noreply.github.com>
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.
GET /characteristics returned the value of every characteristic, including write-only ones like Identify. With Camera Key from the HomeKit Secure Video guide (#531), any paired controller could read a key another controller wrote. Without
pra read now answers -70405 and the getter is not called. The /accessories database already left these values out; there are tests for it now.Subscribing without
evanswers -70406 instead of subscribing, and writing a value withoutpwanswers -70404 instead of calling the setter. A 207 now also lists the subscriptions that went through.A write response (
"r": true) is only sent for characteristics withwr. Withoutprthe response becomes the stored value. Withpr, only that connection can read it back until it closes, and its next write drops it. Other controllers and /accessories keep reading the characteristic's own value, which the write leaves as it was, also when the setter fails or answers nothing, and ev subscribers are not notified of the request. Such writes can carry SDP and stream keys. HAP-NodeJS does these checks too, but lets every controller read the last write response. Where our catalog's pr/pw/ev flags differ from HAP-NodeJS, ours are the more permissive ones, so for the catalog characteristics nothing iOS did before gets refused.AI use: written with the help of an AI coding assistant (Opus 5.5). I reviewed and tested the changes myself (pytest, ruff, ty).