Repository navigation
fix(climate): ask silent F455 zones their state once after startup (#466) - #873
Conversation
|
@GreenGrassBlueOcean, feedback welcome here too. The traces behind every change come from a real F455 on the same SCS bus as our F461 test gateway; details and questions are in #872 (comment above it). |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merge order with #867This PR and #867 both address the #466 symptom: a standalone zone on a gateway that refuses
Proposed order: merge #867 first, then rebase #873 on it. Why:
After #867 is merged I'll rebase this branch, resolve the Generated by Claude Code |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks for putting this together and for capturing the third direct probe run on probe-less zone 3 (F455_direct_requests_zone3_2026-11-06.jsonl), @gdluck — the empirical picture of how the F455 1.0.76 handles *#4*Z## (~9 s ACK), *#4*Z*0/14/12## (~0.1 s ACK with probe vs ~9 s NACK without probe), *#4*Z*20## (<20 ms NACK), and *#4*Z#1*20## (~0.09 s ACK) is crystal clear, and the paced read-by-read design (*0## → *14## → *12## → #1*20##, stopping on the first refused/unanswered read) is the right approach while keeping the #674 guards intact.
Requesting a few targeted changes before we merge — detailed inline below:
- [High]
RestoreEntitysetpoint masks missing setpoint/mode/actuator on warm restarts (climate.py):
async_restore_last_state()restores_target_temperatureand_local_target_temperaturedirectly without a_target_temperature_restoredflag, soself.native_target_temperatureis already non-Noneafter every Home Assistant restart. Whenever a zone emits a routine ambient temperature frame (*#4*Z*0*TTTT##) during the 120 s grace window — Zone 4's exact pattern in your live capture —_current_temperature_restoredflips toFalsewhilenative_target_temperaturestill holds the stale restored setpoint, causing_poll_if_silent()to return early without asking*14##,*12##(mode), or#1*20##(actuator). - [Medium] In-band
WHO 13→WHO 1013identity race atasync_added_to_hass()(climate.py):
async_forward_entry_setups()runs beforegateway.initial_discovery(), so checkingsilent_zone_poll_neededeagerly inasync_added_to_hass()misses gateways configured asGeneric(or mis-selected manually) that resolve toF455via*#1013**1*8*...##during startup discovery — and conversely never re-checkssilent_zone_poll_neededafterwait_for_initial_discovery()if an entry was corrected away fromF455. - [Medium]
_full_whereframe interpolation on#ZYAML zones #IIsyntax +parse()Noneguard (climate.py):
where: "#2"is valid non-central YAML zone syntax (_ZONE_WHEREmatches#2), wheref"*#4*{self._full_where}#1*20##"produces*#4*#2#1*20##(4-zone central unit channel syntax rather than*#4*2#1*20##) and*#4*#2*0/14/12##addresses the central unit rather than probe2. Deriving the bare zone viazone_number(self._where)(and guardingcmd is not Nonerather thancast(OWNCommand, ...)) avoids both. - [Medium] Shared-bus
_who4_target()delegation & follower gating (gateway.py):
silent_zone_poll_neededchecksself._model_keydirectly rather than delegating throughself._who4_target()(unlikehumidity_request_refused/interface_zone_request_refused), and does not suppress passivestandby/ non-WHO-4secondarygateways. - [Low / Optional]
ACTUATOR_REQUEST_REFUSED,HUMIDITY_REQUEST_REFUSED& docs:- Since
F455_direct_requests_2026-11-06.jsonlproves<20 msNACKs on*#4*Z*20##,*#4*0*20##, and*#4*2*60##, consider adding"F455"toACTUATOR_REQUEST_REFUSEDandHUMIDITY_REQUEST_REFUSED(and runningscripts/sync_documentation.py) sowatch_zone_actuatorsstops sending doomed*#4*Z*20##requests on startup and reconnect. - Please add a brief note on the F455 120 s silent-zone poll to
docs/configuration/climate.mdanddocs/configuration/runtime_behaviour.md(andscripts/update_gateway_profiles.pyif appropriate) alongside the existing per-gateway WHO 4 polling notes.
- Since
Merge Order (#872 & #867) + Answer on Standby Pruning (#872)
- Merge order (
#867→#873): Confirmed your analysis in#issuecomment-6706404306— in#867,gateway_sessions.pystill calls_cancel_written(task)unconditionally onnack_rejectedbefore dispatching any partialOWNNackError.responses, and*#4*3*0##on F455 returns 0 partial frames, sowritten.cancelled()stops_poll_if_silentafter the first read identically. Since#872is now merged intov2-phase1-architecture, we'll merge#867first and then you can rebase#873ontov2-phase1-architecture(dropping#872's commit and squashing the intermediateZONE_SETPOINT_POLLrevert commits). - Standby entity pruning when primary is unloaded (
#872question): That behaviour is intentional (#453).PlatformDiscovery._owned_by_primary()checks the persistentEntityRegistry(not whether the primaryConfigEntryis currentlyLOADED), andentry_is_follower()reads directly fromconfig_entrydata/options so setup order or a temporarily unreachable primary (ConfigEntryNotReady) never causes a warm standby to mint duplicate entities or steal ownership. At runtime, when the primary entry is loaded and loses TCP connectivity,_bridge_to_primary()and_failover_target()route RX/TX through the warm standby onto the primary's existing entities. Only when the primary entry is manually disabled/unloaded does the bus go unserved until the standby is reconfigured as primary/standalone (and if the primary entry is deleted,async_check_primary_links()raises theprimary_missingrepair issue).
…ecide it when it runs (OpenWebNet-HA#873 review) Review 5817546241 on 195fd13, point by point: 1. A restored setpoint or mode is not a report. async_restore_last_state now flags _target_temperature_restored and _hvac_mode_restored like the restored temperature; dimension 14 / 12 frames, mode frames and a season frame that gives a mode clear them. _silent_zone_reads() asks only for what is missing: *0 when the temperature is not live, *14 when the setpoint is not, *12 when the setpoint or the mode is not, #1*20 when no actuator state is known. 2. The gate is read inside the task, after wait_for_initial_discovery() and the grace sleep, as watch_zone_actuators does (2e647e7): the in-band identity may correct the model after the entities were added. The task is scheduled when the gateway wants the poll at add time or while the startup sweep is still pending. 3. The reads are built from zone_number(self._where) and the routing, actuator token first: *michnovka#4*2*0## for a "#2" zone, *michnovka#4*2#1#4#01*20## behind an interface. A transport error in the loop is logged (OpenWebNet-HA#593). 4. silent_zone_poll_needed goes through _who4_target() like humidity_request_refused (c889731) and is False for a follower without WHO 4 in delegated_whos. 5. HUMIDITY_REQUEST_REFUSED gains F455 (live 1.0.76: *michnovka#4*2*60## NACKed in 19 ms with nothing on the bus). ACTUATOR_REQUEST_REFUSED is left alone because it also gates the thermo actuator switches, whose *michnovka#4*Z#N*20## the F455 answers in 0.09 s; the new ZONE_ACTUATOR_REQUEST_REFUSED = {"F455"} drops only the zone form *michnovka#4*Z*20## (NACKed in 2-18 ms) in _send_zone_requests. Docs notes in climate.md and runtime_behaviour.md. The six humidity-gate tests that used the F455 as their unlisted model use the MH201; the OpenWebNet-HA#674 replay asserts zone 42's humidity keeps coming from its own pushes. 44 tests in test_f455_zone_polling.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TwNCwPHjW6sW8g1sMd4zJW
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Great work on the F455 silent-zone poll and capture verification! Everything passes except the strict 100.0% coverage gate on two branches in custom_components/myhome/gateway.py (line 704 when a dimension-20 batch on F455 contains only zone-level addresses, and line 762 when zone_actuator_request_refused delegates via _who4_target()).
Here is a 1-click suggestion covering both lines so CI goes green.
|
I'll be back at PC in 1h to fix it. |
|
Thanks for the review. Each point is addressed in dee21dc on top of 195fd13, with a test per point in 1. Restored setpoint and mode (high). 2. Identity race (medium). The gate is read inside the task, after 3. 4. 5. Tables and docs (low).
Merge order. Agreed: #867 first, then this branch rebased onto Run on Python 3.14 / HA 2026.10.4 / OWNd 2.0.0b13: full Generated by Claude Code |
…ed zone-form refusal (OpenWebNet-HA#873 review) Two branches of gateway.py were not exercised: `_send_zone_requests` dropping a dimension 20 batch that holds only zone-level addresses on an F455 (nothing left to ask once the zone form is refused), and `zone_actuator_request_refused` read through `_who4_target()` when another gateway serves WHO 4. Both are now asserted in test_f455_zone_polling.py, as suggested in the review. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…p sweep (OpenWebNet-HA#466) A physical F455 1.0.76 answers the single-dimension reads *michnovka#4*Z*0##, *michnovka#4*Z*14## and *michnovka#4*Z*12## in ~0.1 s and the actuator form *michnovka#4*Z#1*20## in 0.09 s, but ACKs *michnovka#4*Z## only after ~9 s (the issue_674 timeouts) and returns nothing for *michnovka#4*0## (tests/fixtures/traces/issue_466/F455_direct_requests_2026-11-06.jsonl); its standalone zone 2 reported nothing on its own in four minutes of debug log. The F455 was on neither the zone status poll nor the temperature poll, so a quiet zone had no temperature, setpoint or mode after a restart. Nothing the issue_674 fixes decided is undone: F455 zones still ask nothing on add, stay off the *michnovka#4*Z## and *michnovka#4*Z*0## polls, and the startup sweep is unchanged. Added: - SILENT_ZONE_POLL = {"F455"} and SILENT_ZONE_GRACE = 120 s (gateway.py, with silent_zone_poll_needed). - MyHOMEClimate._poll_if_silent: 120 s after the startup sweep, a zone still without a temperature (a restored one does not count) or a setpoint of its own asks *michnovka#4*Z*0##, then *michnovka#4*Z*14##, *michnovka#4*Z*12## (its reply is followed by the zone's mode frame) and *michnovka#4*Z#1*20##, each only once the previous read was answered; a refused or unanswered read ends it, so a zone without a probe costs one request. The actuator is asked last because hvac_action is derived when an actuator report finds the mode known. tests/test_f455_zone_polling.py ties the unchanged gates, the capture behind each read and the poll itself together. Squash of the three development commits of OpenWebNet-HA#873 (the first two added F455 to the poll tables and a ZONE_SETPOINT_POLL, the third reverted that in favour of this mechanism), rebased on OpenWebNet-HA#867 and its ZONE_TEMPERATURE_POLL table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ancel CI on 6b8af43: the HA beta/dev lanes flagged `current_temperature` and `target_temperature` as deprecated reads inside _poll_if_silent (use the native attributes), and the strict coverage gate found the removal callback that cancels a waiting poll never executed. The poll now reads _attr_native_current_temperature / native_target_temperature, and the scheduling test runs the entity's on-remove callbacks and asserts the task is cancelled. 5407 tests, 100 % coverage, both OWNd lanes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a probe; pin it for the silent poll A third direct run on the physical F455 1.0.76 (20:55Z, same day, same probe tool) against zone 3, an actuator channel with no probe: *michnovka#4*3*0##, *michnovka#4*3*14##, *michnovka#4*3*12## and *michnovka#4*3*19## are each NACKed only after 8.96-9.01 s with no frame, *michnovka#4*3## ACKs after 8.99 s with none, *michnovka#4*3*20## NACKs in 3 ms and *michnovka#4*3#1*20## answers *michnovka#4*3#1*20*0## in 0.09 s. The populated zones answer *michnovka#4*Z*0## in 0.10-0.13 s in the same run. This is the record behind the silent poll's "a refused read ends it" path: a probe-less zone costs one read, and that read holds a command session for ~9 s, once per zone added. The SILENT_ZONE_POLL comment, the fixture README and tests/test_f455_zone_polling.py say so and assert the timings; the trace test pins the new file's SHA-256 and checks it carries no real address. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TwNCwPHjW6sW8g1sMd4zJW
…ecide it when it runs (OpenWebNet-HA#873 review) Review 5817546241 on 195fd13, point by point: 1. A restored setpoint or mode is not a report. async_restore_last_state now flags _target_temperature_restored and _hvac_mode_restored like the restored temperature; dimension 14 / 12 frames, mode frames and a season frame that gives a mode clear them. _silent_zone_reads() asks only for what is missing: *0 when the temperature is not live, *14 when the setpoint is not, *12 when the setpoint or the mode is not, #1*20 when no actuator state is known. 2. The gate is read inside the task, after wait_for_initial_discovery() and the grace sleep, as watch_zone_actuators does (2e647e7): the in-band identity may correct the model after the entities were added. The task is scheduled when the gateway wants the poll at add time or while the startup sweep is still pending. 3. The reads are built from zone_number(self._where) and the routing, actuator token first: *michnovka#4*2*0## for a "#2" zone, *michnovka#4*2#1#4#01*20## behind an interface. A transport error in the loop is logged (OpenWebNet-HA#593). 4. silent_zone_poll_needed goes through _who4_target() like humidity_request_refused (c889731) and is False for a follower without WHO 4 in delegated_whos. 5. HUMIDITY_REQUEST_REFUSED gains F455 (live 1.0.76: *michnovka#4*2*60## NACKed in 19 ms with nothing on the bus). ACTUATOR_REQUEST_REFUSED is left alone because it also gates the thermo actuator switches, whose *michnovka#4*Z#N*20## the F455 answers in 0.09 s; the new ZONE_ACTUATOR_REQUEST_REFUSED = {"F455"} drops only the zone form *michnovka#4*Z*20## (NACKed in 2-18 ms) in _send_zone_requests. Docs notes in climate.md and runtime_behaviour.md. The six humidity-gate tests that used the F455 as their unlisted model use the MH201; the OpenWebNet-HA#674 replay asserts zone 42's humidity keeps coming from its own pushes. 44 tests in test_f455_zone_polling.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TwNCwPHjW6sW8g1sMd4zJW
…ed zone-form refusal (OpenWebNet-HA#873 review) Two branches of gateway.py were not exercised: `_send_zone_requests` dropping a dimension 20 batch that holds only zone-level addresses on an F455 (nothing left to ask once the zone form is refused), and `zone_actuator_request_refused` read through `_who4_target()` when another gateway serves WHO 4. Both are now asserted in test_f455_zone_polling.py, as suggested in the review. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
58c1074 to
d21fba9
Compare
|
Rebased on
Run here on the rebased branch (Python 3.14 / HA 2026.11 / OWNd 2.0.0b13): full Generated by Claude Code |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks for the clean rebase onto v2-phase1-architecture and for addressing every review item so thoroughly, @gdluck!
Verified on d21fba9:
RestoreEntitywarm-restart state:_target_temperature_restoredand_hvac_mode_restoredcleanly distinguish persisted pre-restart values from live session frames so_silent_zone_reads()asks only the dimensions still missing att = 120 swhile keeping#185state display intact.- In-band
WHO 13/WHO 1013identity resolution: Scheduling whileinitial_discovery_pendingisTrueand evaluatingsilent_zone_poll_neededafterwait_for_initial_discovery()+ grace avoids both false negatives (Generic→F455) and false positives (F455→ another model). - Address formatting & shared-bus routing:
zone_number(self._where)handles#ZYAML zones andZ#1#4#II*20interface ordering cleanly, and bothsilent_zone_poll_neededandzone_actuator_request_refuseddelegate via_who4_target()while suppressing passive followers. - Refusal tables, docs & test suite: Splitting
ZONE_ACTUATOR_REQUEST_REFUSED = {"F455"}fromACTUATOR_REQUEST_REFUSEDis a great catch so thermo actuator switches (Z#N) keep working on F455 while doomed*#4*Z*20##and*#4*Z*60##reads are dropped, and all 16 CI checks (including 100.0% coverage across 44 tests intests/test_f455_zone_polling.py) are green.
LGTM — merging now! 🚀
be78bd7
into
OpenWebNet-HA:v2-phase1-architecture
Requested by A
What is the problem
On an F455, a heating zone that does not report by itself has no temperature, setpoint, mode or action after a restart. Since issue_674 the F455 zones ask nothing on add: they are off the zone status poll
*#4*Z##, because sixteen of them timing out one after another locked the command session for 161 s, and off the temperature poll. That decision is right for zones that report on their own. A standalone thermostat that stays quiet, though, stays empty. The live F455 capture in #872 shows this: on the test plant the standalone zone 2 reported nothing on its own in the four-minute debug log.Part of #466. Depends on #872 (the trace corpus). This branch is built on it, so its first commit is #872's. After review here, the branch was reworked so it only adds behaviour. Please review the net diff against #872 (
climate.py,gateway.py, the new test file). The intermediate commits are superseded by the last one.What is the solution
What stays exactly as it was (no existing table, test or document changes):
*#4*Z##and the*#4*Z*0##temperature poll.*#4*0##.*#4*Z*20##actuator watch is unchanged.What is added:
SILENT_ZONE_POLL = {"F455"},SILENT_ZONE_GRACE = 120.0andMyHOMEGatewayHandler.silent_zone_poll_needed(custom_components/myhome/gateway.py).MyHOMEClimate._poll_if_silent(custom_components/myhome/climate.py). It is scheduled when a zone is added on such a gateway, and skipped for interface and actuator addresses. 120 s after the startup sweep it checks whether the zone has a temperature (a restored one does not count) and a setpoint of its own. If either is missing, it asks, one read at a time and each only after the previous one was answered:*#4*Z*0##for the temperature;*#4*Z*14##for the setpoint;*#4*Z*12##, whose reply is followed by the zone's mode frame;*#4*Z#1*20##for actuator 1, asked last becausehvac_actionis derived when an actuator report finds the mode known.A refused or unanswered read ends the sequence, so a zone without a probe costs one request. Zones that report by themselves are never asked.
What a probe-less zone costs, measured on the F455 (third direct run,
tests/fixtures/traces/issue_466/F455_direct_requests_zone3_2026-11-06.jsonl): on zone 3, an actuator channel with no probe,*#4*3*0##,*#4*3*14##,*#4*3*12##and*#4*3*19##are each NACKed only after 8.96–9.01 s, with no frame;*#4*3*20##NACKs in 3 ms and*#4*3#1*20##answers in 0.09 s. So the silent poll holds a command session for about 9 s once per probe-less zone, at startup only, and nothing more. On the issue_674 plant, where the 2026-10-09 log shows 13 of the 16 zones that were polled are of that kind, that is 13 × 9 s spread over the two F455 command sessions, against the 161 s serial lockup the*#4*Z##polls caused there.tests/test_f455_zone_polling.pyasserts these timings from the capture.State Diagram
flowchart LR A["F455 zone added"] --> N["no request on add, as since 674"] N --> W["wait for startup sweep plus 120 s"] W --> K{"temperature and setpoint known"} K -- "yes" --> E["nothing asked"] K -- "no" --> T["ask dimension 0"] T -- "refused or unanswered" --> E T -- "answered" --> S["ask dimension 14"] S -- "answered" --> M["ask dimension 12 then mode frame"] M -- "answered" --> AC["ask actuator Z#1 dimension 20"]Hardware Verification
F455_direct_requests_2026-11-06.jsonl, F455 1.0.76): each of the four reads is answered on the command session in 0.09–0.12 s for zones 2 and 4, and*#4*Z*12##returns*#4*Z*12*T*3##followed by*4*0*Z##.*#4*Z##ACKs only after ~9 s, which is why the F455 stays off that poll.*#4*0##,*#4*#0##, and*#4*Z*20##NACKed in milliseconds.*#4*2*0##,*14##,*12##+*4*0*2##, and*#4*2#1*20##, all answered.*#4*3*0##was NACKed after 9 s, and nothing more was asked for it.cool, 23.8 °C, setpoint 22.0 °C, actioncooling. No MyHOME error in the log.test_a_zone_with_a_temperature_but_no_setpoint_is_asked.How is it tested
tests/test_f455_zone_polling.py(new, 22 cases):supports_zone_status,zone_temperature_poll_needed,general_zone_request_refusedandactuator_request_refusedare all False, and*#4*0##stays in the sweep.ruff check, the mypy ratchet,scripts/sync_documentation.py --checkandscripts/verify_ha_standards.pypass.Where are the traces
tests/fixtures/traces/issue_466/F455_direct_requests_2026-11-06.jsonlandtests/fixtures/traces/issue_466/F455_full_debug_log_2026-11-06T16-24-46.log, added by #872, plustests/fixtures/traces/issue_466/F455_direct_requests_zone3_2026-11-06.jsonl(the probe-less zone, added here with its README section; SHA-256 pinned, no real address).What does the Firmware Oracle / Encyclopedia say
WHO = 4,who-4-temperature-control/dimensions.md): dimensions 0, 12 and 14 are per-zone reads, and the dimension 12 response comes with a separate*4*WHAT*WHERE##state frame. Dimension 20 is published as*#4*Z#N*20##("Where actuators = Z#N" in the WHO 4 PDF), the form the F455 answers.findings/F455/emulation-strategy.mdonly), so the live capture is the evidence.Checklist
handle_event().pytest(on OWNd b13 and dev),ruff, mypy ratchet, documentation sync and the HA standards check pass.🤖 Generated with Claude Code