Skip to content

fix(climate): ask silent F455 zones their state once after startup (#466) - #873

Merged
GreenGrassBlueOcean merged 5 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/f455-zone-polling
Nov 8, 2026
Merged

GreenGrassBlueOcean merged 5 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/f455-zone-polling

Conversation

@gdluck

@gdluck gdluck commented Nov 6, 2026 •

Copy link
Copy Markdown

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):

  • F455 zones still ask nothing on add.
  • They stay off *#4*Z## and the *#4*Z*0## temperature poll.
  • The startup sweep still sends *#4*0##.
  • The *#4*Z*20## actuator watch is unchanged.

What is added:

  1. SILENT_ZONE_POLL = {"F455"}, SILENT_ZONE_GRACE = 120.0 and MyHOMEGatewayHandler.silent_zone_poll_needed (custom_components/myhome/gateway.py).

  2. 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:

    1. *#4*Z*0## for the temperature;
    2. *#4*Z*14## for the setpoint;
    3. *#4*Z*12##, whose reply is followed by the zone's mode frame;
    4. *#4*Z#1*20## for actuator 1, asked last because hvac_action is 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.py asserts 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"]
Loading

Hardware Verification

  • Capture (test(#466): Legrand F455 1.0.76 live drive and direct requests (2026-11-06) #872, 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.
  • Run live on a local Home Assistant 2026.10 with OWNd 2.0.0b13 and an F455 entry on this branch, against the same physical F455, 2026-11-06 20:14–20:17 local:
    • Startup is identical to before: the sweep including *#4*0##, *#4*#0##, and *#4*Z*20## NACKed in milliseconds.
    • Zone 4 reported its temperature by itself at 20:15:00 but no setpoint.
    • 120 s after the sweep, Thermo ofice (zone 2) and N4692FAN (zone 3) were silent:
      • Zone 2 got *#4*2*0##, *14##, *12## + *4*0*2##, and *#4*2#1*20##, all answered.
      • Zone 3 has no probe: its *#4*3*0## was NACKed after 9 s, and nothing more was asked for it.
    • Result: Thermo ofice cool, 23.8 °C, setpoint 22.0 °C, action cooling. No MyHOME error in the log.
    • That run predates the last refinement, which also asks a zone that has a temperature but no setpoint (zone 4's case). It is covered by test_a_zone_with_a_temperature_but_no_setpoint_is_asked.

How is it tested

  • tests/test_f455_zone_polling.py (new, 22 cases):
    • The capture behind every read.
    • The unchanged F455 multiple timeouts #674 gates on the F455 handler: supports_zone_status, zone_temperature_poll_needed, general_zone_request_refused and actuator_request_refused are all False, and *#4*0## stays in the sweep.
    • No other gateway gets the poll, and an F455 zone still asks nothing on add or update.
    • The poll order, with a restored temperature counting as silent.
    • Zones that reported temperature and setpoint are skipped, while a zone with only a temperature is asked.
    • Each read waits for the previous answer, and a refused first read ends the poll; the zone 3 capture backs the ~9 s cost of that one refused read.
    • The captured replies in poll order give mode and action.
    • The poll is scheduled for an added F455 zone and not for an MH202 zone.
  • Full suite: 5407 passed on the pinned OWNd 2.0.0b13 and on dev OWNd, with no existing test changed. ruff check, the mypy ratchet, scripts/sync_documentation.py --check and scripts/verify_ha_standards.py pass.

Where are the traces

tests/fixtures/traces/issue_466/F455_direct_requests_2026-11-06.jsonl and tests/fixtures/traces/issue_466/F455_full_debug_log_2026-11-06T16-24-46.log, added by #872, plus tests/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

  1. OpenWebNet Encyclopedia (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.
  2. Firmware Oracle: there is no F455 image (findings/F455/emulation-strategy.md only), so the live capture is the evidence.

Checklist

  • PR Description: Follows the structured sections above.
  • Trace-Backed (if protocol/device change): Every read is backed by the committed F455 capture of test(#466): Legrand F455 1.0.76 live drive and direct requests (2026-11-06) #872.
  • V2 Architecture: No new subscriptions; climate keeps handle_event().
  • Tests & Linters: pytest (on OWNd b13 and dev), ruff, mypy ratchet, documentation sync and the HA standards check pass.

🤖 Generated with Claude Code

@gdluck

gdluck commented Nov 6, 2026

Copy link
Copy Markdown
Author

@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-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@gdluck gdluck changed the title fix(climate): poll F455 zones with single-dimension reads (#466) fix(climate): ask silent F455 zones their state once after startup (#466) Nov 6, 2026
@gdluck

gdluck commented Nov 7, 2026

Copy link
Copy Markdown
Author

Merge order with #867

This PR and #867 both address the #466 symptom: a standalone zone on a gateway that refuses *#4*0## has no setpoint or mode after a restart. They do not duplicate each other. They cover different gateways and take opposite approaches, on purpose.

#867 #873 (this PR)
Gateways F454, MH202 F455 only
What the firmware does with *#4*Z## answers all five frames quickly (F454 ~1.3 s, MH202 ~0.2 s) ACKs only after ~9 s, which caused the #674 timeouts
Approach puts these gateways back on the zone status poll, and keeps the partial replies when a standalone thermostat ends the request with a NACK keeps the F455 off that poll; a zone still silent 120 s after the sweep asks once with single reads (*0, *14, *12, #1*20, ~0.1 s each)

Proposed order: merge #867 first, then rebase #873 on it.

Why:

  1. fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) #867 is the broader change. It moves F454 and MH202 out of ZONE_TEMPERATURE_POLL and the supports_zone_status fallback list, and lets the OWNd profile flag supports_heating_zone_status decide that poll (OWNd#164). fix(climate): ask silent F455 zones their state once after startup (#466) #873 only adds one F455 table and one entity method, so the smaller change goes on top.
  2. The only conflict is textual and sits on fix(climate): ask silent F455 zones their state once after startup (#466) #873's side. Both PRs edit the comment and table just above ZONE_TEMPERATURE_POLL in custom_components/myhome/gateway.py. A test merge of fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) #867 into fix(climate): ask silent F455 zones their state once after startup (#466) #873 conflicts only there. The resolution is to take fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) #867's table and comment, then keep fix(climate): ask silent F455 zones their state once after startup (#466) #873's SILENT_ZONE_POLL / SILENT_ZONE_GRACE block below it. Nothing else conflicts.
  3. fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) #867 keeps what fix(climate): ask silent F455 zones their state once after startup (#466) #873 depends on.

After #867 is merged I'll rebase this branch, resolve the gateway.py hunk as described, and re-run the full suite before pushing.


Generated by Claude Code

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. [High] RestoreEntity setpoint masks missing setpoint/mode/actuator on warm restarts (climate.py):
    async_restore_last_state() restores _target_temperature and _local_target_temperature directly without a _target_temperature_restored flag, so self.native_target_temperature is already non-None after 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_restored flips to False while native_target_temperature still holds the stale restored setpoint, causing _poll_if_silent() to return early without asking *14##, *12## (mode), or #1*20## (actuator).
  2. [Medium] In-band WHO 13 → WHO 1013 identity race at async_added_to_hass() (climate.py):
    async_forward_entry_setups() runs before gateway.initial_discovery(), so checking silent_zone_poll_needed eagerly in async_added_to_hass() misses gateways configured as Generic (or mis-selected manually) that resolve to F455 via *#1013**1*8*...## during startup discovery — and conversely never re-checks silent_zone_poll_needed after wait_for_initial_discovery() if an entry was corrected away from F455.
  3. [Medium] _full_where frame interpolation on #Z YAML zones & #4#II syntax + parse() None guard (climate.py):
    where: "#2" is valid non-central YAML zone syntax (_ZONE_WHERE matches #2), where f"*#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 probe 2. Deriving the bare zone via zone_number(self._where) (and guarding cmd is not None rather than cast(OWNCommand, ...)) avoids both.
  4. [Medium] Shared-bus _who4_target() delegation & follower gating (gateway.py):
    silent_zone_poll_needed checks self._model_key directly rather than delegating through self._who4_target() (unlike humidity_request_refused / interface_zone_request_refused), and does not suppress passive standby / non-WHO-4 secondary gateways.
  5. [Low / Optional] ACTUATOR_REQUEST_REFUSED, HUMIDITY_REQUEST_REFUSED & docs:
    • Since F455_direct_requests_2026-11-06.jsonl proves <20 ms NACKs on *#4*Z*20##, *#4*0*20##, and *#4*2*60##, consider adding "F455" to ACTUATOR_REQUEST_REFUSED and HUMIDITY_REQUEST_REFUSED (and running scripts/sync_documentation.py) so watch_zone_actuators stops 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.md and docs/configuration/runtime_behaviour.md (and scripts/update_gateway_profiles.py if appropriate) alongside the existing per-gateway WHO 4 polling notes.

Merge Order (#872 & #867) + Answer on Standby Pruning (#872)

  • Merge order (#867 → #873): Confirmed your analysis in #issuecomment-6706404306 — in #867, gateway_sessions.py still calls _cancel_written(task) unconditionally on nack_rejected before dispatching any partial OWNNackError.responses, and *#4*3*0## on F455 returns 0 partial frames, so written.cancelled() stops _poll_if_silent after the first read identically. Since #872 is now merged into v2-phase1-architecture, we'll merge #867 first and then you can rebase #873 onto v2-phase1-architecture (dropping #872's commit and squashing the intermediate ZONE_SETPOINT_POLL revert commits).
  • Standby entity pruning when primary is unloaded (#872 question): That behaviour is intentional (#453). PlatformDiscovery._owned_by_primary() checks the persistent EntityRegistry (not whether the primary ConfigEntry is currently LOADED), and entry_is_follower() reads directly from config_entry data/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 the primary_missing repair issue).

Comment thread custom_components/myhome/climate.py Resolved
Comment thread custom_components/myhome/climate.py Resolved
Comment thread custom_components/myhome/climate.py Resolved
Comment thread custom_components/myhome/gateway.py Resolved
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Nov 8, 2026
…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
@gdluck
gdluck marked this pull request as draft November 8, 2026 11:55
@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review November 8, 2026 12:19

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/test_f455_zone_polling.py Resolved
@gdluck

gdluck commented Nov 8, 2026

Copy link
Copy Markdown
Author

I'll be back at PC in 1h to fix it.

@gdluck

gdluck commented Nov 8, 2026

Copy link
Copy Markdown
Author

Thanks for the review. Each point is addressed in dee21dc on top of 195fd13, with a test per point in tests/test_f455_zone_polling.py.

1. Restored setpoint and mode (high). async_restore_last_state now sets _target_temperature_restored and _hvac_mode_restored, the same way it already flags the restored temperature. Dimension 14 / 12 frames clear the first, mode frames (*4*110*Z##, *4*210#TTTT*Z##, *4*303*Z##) and a season frame that gives a mode clear the second. _silent_zone_reads() then asks only for what the zone has not said itself: *0 is skipped when the temperature is live, *14 when the setpoint is, *12 when both the setpoint and the mode are, and #1*20 goes out whenever _actuator_states is empty and _zone_active is None. Tests: a zone restored heat / 21.0 that pushes only its temperature in the grace period is asked *14, *12, #1*20; a restored zone that pushes nothing is asked all four; each live frame clears its flag; the restored values are still what the entity shows until then (#185).

2. Identity race (medium). The gate is read inside the task, after wait_for_initial_discovery() and the grace sleep, as watch_zone_actuators does since 2e647e7. The task is scheduled when the gateway wants the poll at add time or while initial_discovery_pending is still true (the sweep has not run, so WHO 13 / WHO 1013 may still correct the model); a zone found on the bus after the sweep on a non-F455 schedules nothing. Tests: an entry labelled F454 whose profile becomes F455 before the poll runs is asked; an F455 label corrected to F454 is not.

3. _full_where (medium). The reads are built from zone_number(self._where) plus the routing, with the actuator token before it: *#4*2*0## for a zone: "#2" entry, *#4*2#1#4#01*20## behind interface 01 (only when the profile sets supports_interface_zone_requests; the default profile still gates the poll off). OWNHeatingCommand.parse returns a command for every frame the poll can build on OWNd 2.0.0b13 (checked for the nine shapes involved), so no None guard; the loop now catches a transport error and logs it, as _send_zone_requests does (#593).

4. _who4_target() (medium). silent_zone_poll_needed returns False for a follower without WHO 4 in delegated_whos, and otherwise reads the table through _who4_target()._model_key, like humidity_request_refused since c889731. Tests: an F461 primary whose WHO 4 goes through an F455 polls, the reverse does not; a passive standby does not, a secondary with WHO 4 does.

5. Tables and docs (low).

  • HUMIDITY_REQUEST_REFUSED gains F455: the live F455 1.0.76 NACKed *#4*2*60## in 19 ms with nothing on the bus (F455_direct_requests_2026-11-06.jsonl t=68.6; a read this gateway forwards and nobody answers takes ~9 s, so this one is dropped by the firmware). The six humidity-gate tests that used the F455 as their unlisted model now use the MH201, and the F455 multiple timeouts #674 replay asserts that zone 42's humidity keeps coming from its own pushes with no *#4*42*60##.
  • ACTUATOR_REQUEST_REFUSED is left alone, on purpose: that flag also gates switch.py (async_update of the thermo actuator switches), and the F455 answers the actuator form *#4*Z#N*20## in 0.09 s (*#4*2#1*20##, *#4*4#1*20##, *#4*3#1*20## in the two captures). Adding it would stop reads the gateway answers. Instead a new ZONE_ACTUATOR_REQUEST_REFUSED = {"F455"} makes _send_zone_requests drop only the zone form (*#4*Z*20##, NACKed in 2-18 ms) and keep Z#N; the zone stays in _actuator_zones for a later in-band correction. Test: watch_zone_actuators("2") and ("2#1") on an F455 send *#4*2#1*20## only; an F454 still sends *#4*2*20##.
  • docs/configuration/climate.md (a new "Zones on an F455" bullet and the dimension 20 bullet) and docs/configuration/runtime_behaviour.md (a new section) describe the 120 s poll, the restored-value rule, the ~9 s cost of a zone without a probe and the shared-bus rule. scripts/sync_documentation.py --check passes.

Merge order. Agreed: #867 first, then this branch rebased onto v2-phase1-architecture. #872's commit (f204e95) is already an ancestor of the base, so it falls out of the rebase by itself; I'll squash the ZONE_SETPOINT_POLL add-and-revert commits (5a34ada, 3c95e61, 6b8af43) at the same time, so the history shows only the final mechanism.

Run on Python 3.14 / HA 2026.10.4 / OWNd 2.0.0b13: full tests/ suite (5431 passed), ruff check, scripts/typing_ratchet.py (0 errors), scripts/sync_documentation.py --check.


Generated by Claude Code

gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Nov 8, 2026
…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>
gdluck and others added 5 commits November 8, 2026 16:13
…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>
@gdluck
gdluck force-pushed the fix/f455-zone-polling branch from 58c1074 to d21fba9 Compare November 8, 2026 14:22
@gdluck

gdluck commented Nov 8, 2026

Copy link
Copy Markdown
Author

Rebased on v2-phase1-architecture now that #867 is in (2c8be56), as agreed above.

Run here on the rebased branch (Python 3.14 / HA 2026.11 / OWNd 2.0.0b13): full tests/ suite 5494 passed, ruff check, scripts/typing_ratchet.py (0 errors), scripts/sync_documentation.py --check.


Generated by Claude Code

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clean rebase onto v2-phase1-architecture and for addressing every review item so thoroughly, @gdluck!

Verified on d21fba9:

  • RestoreEntity warm-restart state: _target_temperature_restored and _hvac_mode_restored cleanly distinguish persisted pre-restart values from live session frames so _silent_zone_reads() asks only the dimensions still missing at t = 120 s while keeping #185 state display intact.
  • In-band WHO 13 / WHO 1013 identity resolution: Scheduling while initial_discovery_pending is True and evaluating silent_zone_poll_needed after wait_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 #Z YAML zones and Z#1#4#II*20 interface ordering cleanly, and both silent_zone_poll_needed and zone_actuator_request_refused delegate via _who4_target() while suppressing passive followers.
  • Refusal tables, docs & test suite: Splitting ZONE_ACTUATOR_REQUEST_REFUSED = {"F455"} from ACTUATOR_REQUEST_REFUSED is 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 in tests/test_f455_zone_polling.py) are green.

LGTM — merging now! 🚀

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit be78bd7 into OpenWebNet-HA:v2-phase1-architecture Nov 8, 2026
16 checks passed
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Nov 8, 2026
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.

3 participants