Skip to content

fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) - #867

Merged
GreenGrassBlueOcean merged 3 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
GreenGrassBlueOcean:fix/issue-866-f454-zone-status-poll
Nov 8, 2026
Merged

GreenGrassBlueOcean merged 3 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
GreenGrassBlueOcean:fix/issue-866-f454-zone-status-poll

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What is the problem

On BTicino F454 (003598) and MH202 (03535 / 003535) plants with standalone room thermostats (no #0 99-zone central unit), climate entities initialize current_temperature and actuator status after a Home Assistant startup, while hvac_mode (state) and target_temperature remained unknown / None on cold start until a thermostat spontaneously transmitted a mode or setpoint change:

  1. Both F454 and MH202 firmware silently drop the general heating status sweep *#4*0## (general_zone_request_refused = True, verified in own-firmware-oracle on F454 020051 and MH202 010024, and confirmed on live hardware).
  2. Following #674 (F455 10 s command-session socket hangs on unpopulated actuator-only channels), F454 and MH202 were conservatively placed in supports_zone_status = False and ZONE_TEMPERATURE_POLL, causing startup to request only Dimension 0 (*#4*Z*0##) and Dimension 20 (*#4*Z*20##) instead of the per-zone status request *#4*Z##.
  3. However, both gateways translate *#4*Z## into the four SCS bus queries ($0499ZZ3000 / 12 / 0E / 0D) and answer all five OpenWebNet frames (*#4*Z*0*TTTT##, *#4*Z*12*TTTT*3##, *4*WHAT*Z##, *#4*Z*14*TTTT*3##, *#4*Z*13*OO##):
    • F454 (up to 4 command sessions) answers in ~1.31–1.33 s on both 99-zone #0 central-unit plants (#629, FW 1.0.34) and standalone thermostat plants (#866, FW 2.0, 13 standalone zones).
    • MH202 answers in ~160–250 ms across all zones (#866, FW 1.0, 5 standalone zones).
  4. In addition, if a standalone thermostat answers only a subset of the four bus queries before the gateway terminates the command session with *#*0## (NACK), OWNd returns None from OWNCommandSession.send(..., is_status_request=True) while preserving the partial frames in OWNNackError.responses (_command_session.last_error.responses), which CommandWorkerPool.sending_loop previously discarded when collected is None.

Fixes #866
Companion OWNd PR: OpenWebNet-HA/OWNd#164
Companion trace PR (MH202): #869


What is the solution

  1. Delegate supports_zone_status to GatewayProfile.supports_heating_zone_status and promote F454 and MH202:
    • MyHOMEGatewayHandler.supports_zone_status (custom_components/myhome/gateway.py) checks profile.supports_heating_zone_status first (bool wins when present, aligning with OpenWebNet-HA/OWNd#164) and removes "F454" and "MH202" from the fallback exclusion list so behavior is identical on both OWNd==2.0.0b13 and OWNd >= 2.0.0b14.
    • Removed "F454" and "MH202" from ZONE_TEMPERATURE_POLL (gateway.py); single-session (MH200, MH201, H4684, F450) and uncaptured (F459) SCS-only models retain the fast ~120 ms *#4*Z*0## poll, and F455 remains supports_zone_status = False (#674).
  2. Harvest partial OWNNackError.responses on NACKed status requests:
    • In CommandWorkerPool.sending_loop (custom_components/myhome/gateway_sessions.py), when collected is None and task["is_status_request"] is True, partial replies recorded in _command_session.last_error.responses (OWNNackError.responses) are deduplicated against bus_monitor.has_frame_since(...), recorded, dispatched via myhome_message_<mac>, and bridged to any offline primary while keeping _cancel_written(task) NACK semantics intact. Because dispatcher_send runs synchronously before the written cancellation callback tick, MyHOMEClimate._poll_answered sees self._poll_health.frames != frames_before and never falsely flags a partially-answering standalone thermostat as unresponsive.
  3. Synchronize documentation and hardware compatibility matrices:
    • Updated scripts/update_gateway_profiles.py, docs/configuration/climate.md, docs/configuration/runtime_behaviour.md, and regenerated docs/getting-started/hardware-compatibility.md via scripts/sync_documentation.py --update.
  4. Commit scrubbed live F454 FW 2.0 trace and cold-start replay suite:

State Diagram

flowchart TD
    Start["Startup Sweep Completes on F454 (003598) / MH202 (03535)"] --> SweepSkip["*#4*0## Omitted (general_zone_request_refused = True)"]
    SweepSkip --> ZoneAdd["MyHOMEClimate Zone Added (_poll_on_add = True)"]
    ZoneAdd --> SendPoll["Send *#4*Z## via CommandWorkerPool"]
    SendPoll --> BusExpand["Gateway Expands to 4 SCS Queries ($0499ZZ3000 / 12 / 0E / 0D)"]
    BusExpand -->|"All 4 Answered (F454: ~1.3 s; MH202: ~200 ms)"| Ack["collected = 5 Frames + *#*1## ACK"]
    BusExpand -->|"Subset Answered + *#*0##"| PartialNack["collected = None; OWNNackError.responses Harvested"]
    BusExpand -->|"Unpopulated Zone (e.g. Z=14)"| EmptyNack["collected = None; responses = []"]
    Ack --> Hydrate["Hydrate Temp (Dim 0), Effective Setpoint (Dim 12 > Dim 14), Mode (*4*1*Z##), Offset (Dim 13)"]
    PartialNack --> Hydrate
    EmptyNack --> Backoff["PollHealth Counts Unanswered Poll -> Backoff After 2 Misses"]
Loading

Hardware Verification

  • Verified against @xduss's physical BTicino F454 (FW 2.0, Home Assistant 2026.10.4, MyHOME 2.0.0b17, OWNd 2.0.0b13) with 13 standalone thermostats (WHERE = 1..13, zero #0 central unit) in [Bug]: F454 – Thermostat states and target temperatures not initialized after startup #866:
    • Manual *#4*0## sweeps at 18:51:53 and 18:52:42 produced zero replies on the bus.
    • Manual *#4*2##, *#4*1##, *#4*3##, and *#4*4## queries each returned all 5 frames (Dim 0, Dim 12, *4*1*Z##, Dim 14, Dim 13) within 1.312–1.332 s, confirming full standalone zone hydration without a central unit.
  • Verified against @xduss's physical BTicino MH202 (FW 1.0, Home Assistant 2026.10.4, MyHOME 2.0.0b17, OWNd 2.0.0b13) with 5 standalone thermostats (WHERE = 1..5) in [Bug]: F454 – Thermostat states and target temperatures not initialized after startup #866:
    • Manual *#4*1## through *#4*5## queries each returned all 5 frames within 160–250 ms.
  • Corroborated by #629 (F454 FW 1.0.34 + #0 central unit, where *#4*1##..*#4*15## likewise completed in ~1.3 s each with all 5 frames).

How is it tested

  • tests/test_issue_866_f454_zone_status_trace.py:
    • test_issue_866_trace_archive_metadata_and_who4_invariants: verifies trace metadata, absence of #0, silence after both *#4*0## sweeps, and 1.31–1.34 s 5-frame completion for *#4*1##..*#4*4##.
    • test_f454_and_003598_enable_zone_status_and_skip_general_and_temp_polls: verifies F454, BTicino F454, and 003598 set supports_zone_status = True, zone_temperature_poll_needed = False, omit *#4*0##, and send *#4*1## on async_update().
    • test_issue_866_cold_start_replay_hydrates_standalone_zones_and_preserves_dim12_setpoint: replays all 86 RX frames from a cold start (no RestoreEntity, no #0), verifying HVACMode.HEAT, ambient temperatures, Dimension 12 effective setpoints (22.0 / 23.0 / 22.0 / 21.5 °C) preserved over trailing Dimension 14 (20.0 °C), active actuator state on zone 6, dual actuators on zone 8, dynamic discovery + immediate *#4*13## poll on zone 13, and None state on unpopulated zone 14.
    • test_partial_nack_status_request_harvests_ownd_responses_and_counts_zone_answered: verifies CommandWorkerPool.sending_loop harvests OWNNackError.responses on NACKed status requests, skips frames already recorded on bus_monitor, hydrates zone5, and increments PollHealth.frames.
  • Replay test suite for MH202 (tests/test_issue_866_mh202_zone_status_trace.py) in companion PR test(climate): commit live MH202 FW 1.0 zone status trace and replay test (#866) #869.
  • Updated tests/test_startup_requests_per_firmware.py (verifying MH202 and F454 both skip *#4*0## and temperature polls in favor of *#4*Z##), tests/test_issue_674_f455_climate_trace.py, tests/test_climate_humidity_poll_gate.py, and profile resolution tests.
  • Updated tests/test_issue_466_hl4684_trace.py (calibrating 003535 / 03535 catalogue codes to verify supports_zone_status is True).
  • Verification results (tested against both pinned OWNd==2.0.0b13 and OpenWebNet-HA/OWNd#164):
    • pytest: 100.0% statement coverage across custom_components/myhome (18,695 / 18,695 statements, 0 missed).
    • ruff check custom_components tests scripts: All checks passed!
    • scripts/verify_ha_standards.py: SUCCESS: All Home Assistant architectural standards passed! (54/54 Platinum rules).
    • scripts/sync_documentation.py --check: SUCCESS: All documentation is calibrated and in sync with the codebase!

Where are the traces

  • tests/fixtures/traces/issue_866/myhome_trace_F454_all_2026-11-04T19-00-04.json — 129 frames (86 RX, 43 TX) captured on a physical BTicino F454 (FW 2.0) with 13 standalone heating zones (1..13) and unpopulated zone 14, scrubbed of all network identifiers.
  • tests/fixtures/traces/issue_866/myhome_trace_MH202_all_2026-11-04T20-39-35.json — 71 frames captured on a physical BTicino MH202 (FW 1.0) with 5 standalone heating zones (1..5), scrubbed of all network identifiers (committed in test(climate): commit live MH202 FW 1.0 zone status trace and replay test (#866) #869).

What does the Firmware Oracle / Encyclopedia say

  1. OpenWebNet Encyclopedia (WHO = 4, Machine KB ownkb-build-0.9.0):
    • ownkb:claim:c000439 / c000449 (specified): *#WHO*WHERE## requests current zone status; *#*0## (NACK) is a negative result or failed-sequence terminator.
    • ownkb:claim:c003242 / c003244 (specified): DIMENSION 12 (*#4*WHERE*12##) returns the complete probe status combining effective target temperature with operating context, whereas DIMENSION 14 reports the base manual setpoint.
  2. Firmware Oracle (own-firmware-oracle, mcp_index.json SHA-256 verified):
    • *#4*0## on F454 (020051) and MH202 (010024): NACK (*#*0##) / SILENT (Drop) — zero SCS bus telegrams emitted.
    • *#4*1## on F454 (020051): OUT (Bus) — bt_H4690 emits the 4 SCS queries 24 30 34 39 39 30 31 33 30 30 30 0d (Dim 0), ...31 32 0d (Dim 12 + mode), ...30 45 0d (Dim 14), and ...30 44 0d (Dim 13).
    • *#4*1## on MH202 (010024, target d62d664c1b50): ACK (*#*1##) + OUT (Bus) — coso emits the 4 SCS queries $0499013000 (Dim 0), $0499013012 (Dim 12), $049901300E (Dim 14), and $049901300D (Dim 13).

Checklist

  • PR Description: Follows the structured sections above (What is the problem, What is the solution, State Diagram, How is it tested, Where are the traces, What does the Firmware Oracle / Encyclopedia say).
  • Trace-Backed (if protocol/device change): Behavior is verified against real OpenWebNet bus traces or gateway firmware fixtures rather than guessed.
  • V2 Architecture: New or modified entities implement handle_event() and do not manually subscribe via async_dispatcher_connect.
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

…OpenWebNet-HA#866)

Promote F454 (003598) to supports_zone_status=True with profile flag delegation (OWNd#164), remove F454 from ZONE_TEMPERATURE_POLL so standalone thermostats hydrate hvac_mode and target_temperature on cold start, harvest partial OWNNackError.responses in CommandWorkerPool, and commit live F454 FW 2.0 129-frame trace replay suite.
@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!

…enWebNet-HA#866)

Enable *#4*Z## zone status polls on BTicino MH202 (FW 1.0) alongside F454. Live hardware traces verified that MH202 answers zone status in 160-250 ms across standalone thermostats, while the firmware oracle confirms MH202 translates *#4*1## to SCS and replies. Document compatibility and calibrate test matrix.
@GreenGrassBlueOcean GreenGrassBlueOcean changed the title fix(climate): poll *#4*Z## on F454 and harvest partial NACK responses (#866) fix(climate): poll *#4*Z## on F454 & MH202 and harvest partial NACK responses (#866) Nov 5, 2026
@gdluck

gdluck commented Nov 7, 2026

Copy link
Copy Markdown

I'm not at pc, but I think is a duplicate of my finding with f455: #873 . If so, I think we must merge logic.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 2c8be56 into OpenWebNet-HA:v2-phase1-architecture Nov 8, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the fix/issue-866-f454-zone-status-poll branch November 8, 2026 14:09
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Nov 8, 2026
…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>
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