Skip to content

Keep the Setup Endpoints answer per connection - #544

Merged
ikalchev merged 37 commits into
ikalchev:devfrom
RaHehl:setup-endpoints-per-connection
Nov 10, 2026
Merged

ikalchev merged 37 commits into
ikalchev:devfrom
RaHehl:setup-endpoints-per-connection

Conversation

@RaHehl

@RaHehl RaHehl commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

A controller writes Setup Endpoints and reads the answer back. The answer carries the session's SRTP keys, so it now stays with the connection that wrote, like a write response with #535.

A characteristic with read_back_response keeps what its setter returns, or leaves as the value, for the writing connection, until it writes again or the connection closes. Other connections and /accessories read the characteristic's own value, and the written request is not left as that value either. The same holds for what the setter returns on a write with r to a readable write response characteristic. A write without r stays readable for everyone unless the characteristic has read_back_response.

Camera.set_endpoints() still sets its answer, or a refusal, as the characteristic value and now also returns it. A subclass that calls super() and returns nothing keeps working. The driver keeps that value for the writer only and puts the shared value back. An async setter must return the answer and must not set the value itself, a value it sets is shared with every controller. Called through client_update_value() without the driver, the answer stays the value.

Tested with unit tests (3.10, 3.14), ruff and ty.

AI use: written with the help of an AI coding assistant (Opus 5.5). I reviewed and tested the changes myself (pytest, ruff, ty).

RaHehl added 19 commits November 8, 2026 20:28
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.
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.
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.
@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.86%. Comparing base (6818241) to head (8ff458a).
⚠️ Report is 1 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #544      +/-   ##
==========================================
+ Coverage   91.77%   91.86%   +0.08%     
==========================================
  Files          27       27              
  Lines        4367     4390      +23     
  Branches      558      563       +5     
==========================================
+ Hits         4008     4033      +25     
+ Misses        251      249       -2     
  Partials      108      108              
Files with missing lines Coverage Δ
pyhap/accessory_driver.py 94.15% <100.00%> (+0.40%) ⬆️
pyhap/camera.py 91.94% <100.00%> (+0.01%) ⬆️
pyhap/characteristic.py 97.97% <100.00%> (+0.08%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@RaHehl
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.
A pid of another type, e.g. a list, raised a TypeError instead of
answering -70410.
# Conflicts:
#	pyhap/characteristic.py
A write asks for a response only with "r": true.
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.
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.
RaHehl and others added 10 commits November 10, 2026 14:50
# Conflicts:
#	pyhap/accessory_driver.py
#	tests/test_accessory_driver.py
…connection

# Conflicts:
#	pyhap/camera.py
#	tests/test_accessory_driver.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.
@ikalchev
ikalchev merged commit 546d131 into ikalchev:dev Nov 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants