Skip to content

fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways - #794

Open
gdluck wants to merge 13 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/gateway-startup-requests-per-firmware
Open

gdluck wants to merge 13 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/gateway-startup-requests-per-firmware

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

The startup sweep sends *#4*0## and *#16*0*5## to every gateway whose profile advertises WHO 4 / WHO 16, but on most firmware these two requests are NACKed by the gateway itself and never reach the bus (own-firmware-oracle, published verdicts; confirmed live on the F461 and on a MyHomeServer1, and on a physical MH201 by the committed issue_466 sweep trace). Two consequences: the sweep waits for answers that never come, and on SCS-only gateways (no answer to *#4*Z## from a standalone thermostat) a thermostat stays unknown after a restart until it next reports by itself, which can take a long time (#185 on an MH201, #257 on an F461).

This PR leaves the two requests out on the models whose firmware drops them (startup sweep and myhome.sweep_bus alike), lets each climate zone on those SCS-only gateways ask for its temperature with *#4*Z*0##, and restores current_temperature from the last state with a current_temperature_restored flag until the zone reports again.

Refs #185, #257, #674. No open issue is closed by this change. Cold-start hydration of the subordinate zones through the central unit (#0) is tracked separately in #813; the trace-corpus schema, timestamp causality, issue_674 forensics and fixture privacy-gate follow-ups in #814.

The per-zone *#4*Z*0## request rests on the published thermo-ts10.tsv row (forwarded on all twelve emulated gateways) and on the live F461 (answered in 119 ms on a populated zone), so this PR does not wait on OpenWebNet-HA/own-firmware-oracle#60; that PR is cited only for the F422 interface gate (*#4*Z#4#II*0##), which fails closed where it is unverified.


What is the solution

  1. gateway.py: three refusal properties, the MyHOME fix(gateway): close idle command sessions before the gateway drops them #381 pattern (the per-model knowledge belongs in OWNd's profiles):
    • general_zone_request_refused reads getattr(profile, "supports_general_heating_status", None) first and falls back to GENERAL_ZONE_REQUEST_REFUSED (F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202, MyHomeServer1) when the flag is None or absent. The MH201 is not in the oracle: the entry is the physical issue_466 sweep trace (review point 1, below). No OWNd issue exists for the flag yet; a follow-up OWNd profiles PR should carry it.
    • sound_status_request_refused reads profile.supports_who(16) first and falls back to SOUND_STATUS_REQUEST_REFUSED (F450, F459, F460, F461, MH202, MyHomeServer1). Plain OWNd master still advertises WHO 16 on the F461 and MyHomeServer1 although every WHO 16 frame is refused there; fix(profiles): drop WHO 16 from the F461 and MyHomeServer1 profiles OWNd#142 drops it from those two profiles, after which profile.supports_who(16) alone is enough.
    • interface_zone_request_refused reads getattr(profile, "supports_interface_zone_requests", None) first and falls back to INTERFACE_ZONE_REQUEST_REFUSED (F450, F453, F453AV, F454, F459, F460, F461, MH200N, MH202, MyHomeServer1: the ten images on which *#4*2#4#01*0## is nack, silent in feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60; H4684 and MH200 forward it). The flag belongs in OWNd as well.
  2. _discovery_frames skips a request the gateway refuses (DEBUG log). Unknown and forwarding gateways (F453, F453AV, MH200N, F455, Generic) are unchanged. The WHO 18 meter probes are not touched. myhome.sweep_bus applies the same gate through handler._refused_by_gateway; force: true sends everything, as services.yaml and services.md say (with force: false the service sends no WHO 4 frame at all on the listed gateways and triggers no per-zone poll).
  3. zone_temperature_poll_needed: only on SCS-only gateways (supports_zone_status False). The profile flag wins when it is stated: supports_general_heating_status True means the sweep answers and no per-zone poll is needed, False means it is; only an unverified flag falls back to ZONE_TEMPERATURE_POLL, an explicit table (F450, F454, F459, H4684, MH200, MH201, MH202). F450 is in the supports_zone_status exclusion tuple (its *#4*1## row is the F454's: the request is expanded into four raw SCS dimension queries and the command session ends with a NACK), and F460 is not in the table: it expands *#4*Z## into one request like the F461 ($00499013000 in trace-corpus.tsv) and answers it, so it keeps the zone status poll.
  4. climate.py: on those gateways a zone polls on add with OWNHeatingCommand.get_temperature (*#4*Z*0##) instead of returning early: one bus query, answered in ~120 ms on a populated zone, instead of the four that *#4*Z## expands into (~1.3 s per zone when a central unit answers them all, a NACK after 1.5-2 s when a standalone thermostat answers only some). F455 and other unknown SCS-only gateways still send nothing (the F455 multiple timeouts #674 test is kept). A zone behind an F422 (WHERE Z#4#II) is not polled where interface_zone_request_refused is True (F450, F454, F459, MH202 among the polling models); the H4684, MH200 and MH201 poll *#4*Z#4#II*0##. _polls_temperature is a pure boolean property (built from _temperature_poll_wanted and _interface_poll_refused); async_update writes the one-shot DEBUG line when it withholds the poll, also when a fan or humidity request keeps the update going. No _poll_answered callback is attached to the dimension 0 request on purpose: the *#4*1*0## NACK on 11 of 12 oracle images is the bus TX timeout of an unpopulated zone (the QEMU images have no thermostat wired; the live F461 NACKs its unpopulated zone 1 the same way after 2.07 s), not a verdict on a populated one, and the result stays uncounted until a command-session capture of *#4*Z*0## on an SCS-only gateway exists. current_temperature is restored from the last state and flagged current_temperature_restored: true until the first *#4*Z*0*T##; a malformed restored value is ignored.
  5. discovery.py: a bus-discovered zone on such a gateway keeps its poll on add unless the revealing frame carried its temperature.
  6. HL4684 and the OWNd aliases: _model_key maps the SSDP model string HL4684 (manifest.json; not an OWNd alias yet) to H4684, and supports_zone_status judges the configured string and the model OWNd resolves it to (get_gateway_profile(model).model_name), so HL4684, L4684, NT4684, 003598 (F454) and 03535 (MH202) are treated like the model they stand for instead of polling *#4*Z##.
  7. Tests whose default sweep model was an MH201 drop *#4*0## from their expectations (test_blank_start.py, test_multi_gateway.py, test_multi_gateway_regressions.py; the delegated-WHO 4 follower case moves to an MH200N, whose firmware forwards the request); tests/test_issue_304_310_batch.py: the MH200 sweep expectation drops *#4*0##; tests/test_multi_gateway.py: the MH202 secondary's sweep_bus leaves out *#4*0## and *#16*0*5## (43 status requests instead of 45).
  8. Docs: docs/configuration/runtime_behaviour.md (startup table and Day 0 matrix), gateways.md, climate.md, services.md, and hardware-compatibility.md, whose "Gateway Startup Discovery Capabilities" table now has two WHO columns: the profile's advertised WHOs (validated against the OWNd profile and the oracle, as before) and the requests the sweep actually sends (derived from a per-row sweep_omits list in scripts/update_gateway_profiles.py, which a test compares with _discovery_frames() for every row). MH200 and MH200N are separate rows, the MH201 row names its *#4*0## refusal and per-zone *#4*<WHERE>*0## poll, the H4684 / HL4684 has a row. Plus the b16 release-notes delta.

Companion PR: OpenWebNet-HA/OWNd#142 (draft): drops WHO 16 from the F461 and MyHomeServer1 profiles (the WHO 16 half of this PR becomes profile.supports_who(16) alone once it is released). The MyHOME companion of OpenWebNet-HA/OWNd#142 is #808 (draft): the discovery rows are derived from profile.supports_who, so it is green against the pinned OWNd and against OpenWebNet-HA/OWNd#142.


State Diagram

stateDiagram-v2
    direction TB

    state "1. Startup sweep and myhome.sweep_bus" as Sweep {
        [*] --> Request : *#4*0## or *#16*0*5##
        Request --> Sent : profile advertises the WHO and the firmware forwards it, before - always
        Request --> Sent : sweep_bus with force true
        Request --> Skipped : *#4*0## with supports_general_heating_status False, or None and model in GENERAL_ZONE_REQUEST_REFUSED, DEBUG
        Request --> Skipped : *#16*0*5## with no WHO 16 in the profile, or model in SOUND_STATUS_REQUEST_REFUSED, DEBUG
    }

    state "2. SCS-only zone without *#4*Z##, before - unknown until it reports" as Zone {
        [*] --> Restored : last state had current_temperature, current_temperature_restored true
        [*] --> Unknown : nothing restored
        Restored --> Polling : zone_temperature_poll_needed, flag False, or None and model in ZONE_TEMPERATURE_POLL
        Unknown --> Polling : zone_temperature_poll_needed
        Restored --> Waiting : zone behind an F422 and interface_zone_request_refused, DEBUG once on update
        Unknown --> Waiting : same gate
        Polling --> Reported : *#4*Z*0## answered *#4*Z*0*T##, flag cleared
        Waiting --> Reported : the zone reports by itself
        Restored --> Reported : the zone reports by itself
    }
Loading

Hardware Verification

  • F461 firmware 2.0.11, 2026-10-23 / 2026-10-24: *#4*0##, *#16*0*5## (and the other 17 WHO 16 forms), *#5*0## are refused by the gateway itself in under 10 ms; *#4*2*0## on the populated zone 2 is answered with *#4*2*0*0239## and an ACK in 119 ms, while *#4*1*0## on the unpopulated zone 1 ends with a NACK after 2.07 s (the bus TX timeout the oracle images show on every zone). With this PR the sweep sends *#1*0##, *#2*0## and the WHO 18 probes, no *#4*0## and no *#16*0*5##; right after a restart current_temperature shows the last value with current_temperature_restored: true, cleared at the first *#4*2*0*…##.
  • MyHomeServer1 firmware 3.87.13 (the oracle emulates 2.82.06), 2026-10-24: *#4*0## NACK in 51 ms (nothing on the event session), the MyHomeServer1 row of the general-zone table confirmed on 3.87.13; *#16*0*5## and *#16*101*5## NACK in 3 ms; per-zone *#4*35## is answered (5 frames), so the MyHomeServer1 keeps its zone poll and needs no temperature poll; its zones 35..70 report *#4*Z*0*TTTT## by themselves.
  • MH201 firmware 3.3.6 (WHO 13 code 200), committed issue_466 captures: the sweep trace sent *#4*0## (frame 11) and no zone frame followed (the only WHO 4 traffic afterwards is three *#4*0#0*17## broadcasts and zone 105's dimension 15), while the sibling trace of the same plant shows zones 2, 3 and 4 reporting (*#4*4*0*0241##, *#4*2*14*0100*3##, *#4*3*12*0090*3##). That is the MH201 entry of GENERAL_ZONE_REQUEST_REFUSED. The MH201 3.6.27 fresh-discovery log in the repository carries no WHO 4 traffic at all (the plant has no climate zone configured), so it neither confirms nor contradicts it.
  • The per-zone temperature poll on an actual MH201 / F450 / F454 / F459 / H4684 / MH200 / MH202 was not captured: those models rest on the oracle rows, the Climate error #185 field report and the F461's answer to the same request. No F422-routed zone was captured either; the interface gate rests on feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60 alone.

Live retest (2026-10-24)

Consensus label: confirmed for this startup configuration (one F461, standalone zone 2; other models not tested). Four starts of a local Home Assistant with the combined branches. The HA debug log is the evidence for the requests that were not sent: at each start it logged Skipping `*#4*0##`: F461 firmware refuses it., Skipping WHO=5 discovery: not supported by F461 profile. and Skipping WHO=16 discovery: not supported by F461 profile. (the WHO 16 line comes from OpenWebNet-HA/OWNd#142's profile, installed in that HA), and no *#4*0##, *#16*0*5## or *#5*0## appears among its sent frames; the bus monitor history of the same starts shows the sent requests *#1*0##, *#2*0##, *#18*51*51##, *#18*51*113##, *#2*05##, *#4*2##, *#4*2*11##, *#4*2*60##, *#4*#0*20##. Zone 2 showed current_temperature: 24.0 from *#4*2*0*0240## and no current_temperature_restored flag (the zone had reported). The *#4*2*11## NACK at every poll is pre-existing upstream code (climate.py, fan-coil dimension), not touched here. The review follow-up commits change nothing on the F461 path (its sweep, zone poll and sweep_bus gating are covered by test_f461_sweep_in_full and test_sweep_bus_leaves_out_the_refused_requests_unless_forced); they were not retested live.


How is it tested

  • tests/test_startup_requests_per_firmware.py (70 cases): the sweep per model for *#4*0## and *#16*0*5## (15 models, MH201 and HL4684 included), the F461 sweep in full, the hardware-compatibility.md sweep column against _discovery_frames() for every row, myhome.sweep_bus on an F461 with force False (*#1*0##, *#2*0##) and True (all five general requests), the two profile-flag overrides, the zone_temperature_poll_needed table (15 models), test_profile_flag_wins_over_the_zone_temperature_poll_table (×4), the HL4684 key and the aliased spellings L4684 / NT4684 / 003598 / 03535, MH201 and MH202 zones send *#4*3*0##, an F422-routed zone on F450 / F454 / F459 / MH202 sends nothing and is logged once at DEBUG by async_update (nothing by __init__ or the property; also when a humidity request keeps the update going) while H4684 / MH200 / MH201 send *#4*3#4#01*0##, the interface profile flag over the table both ways, an F455 zone sends nothing, a central unit never polls temperature, a bus-discovered zone keeps its poll until a temperature arrives, the restored temperature and its flag, a malformed restored value, a cancelled dimension 0 write never reaches _poll_answered.
  • Captured frames (same module): the three archives below are loaded with the loaders the other trace tests use. f461_live_2026-10-23 run 1: *#4*2*0## answered with *#4*2*0*0239## and an ACK in 0.119 s, *#4*1*0## NACKed after 2.071 s, *#4*0## refused locally; an MH202 zone 2 sends exactly the captured *#4*2*0## and the captured reply clears its restored temperature. myhomeserver1_live_2026-10-24: *#4*0## refused in 0.051 s, *#4*35## answered with five frames, the 33 zones 35..70 of run 1 replayed through zones on a MyHomeServer1 handler (no temperature poll, *#4*35## kept). issue_466 MH201 sweep: frame 11 *#4*0## with no zone frame afterwards, the sibling trace's zones 2, 3 and 4; an MH201 handler leaves *#4*0## out and its zone 4 polls *#4*4*0##, the captured *#4*4*0*0241## gives 24.1. The synthetic zone 3 cases stay: they pin the request a zone sends, not a plant.
  • tests/test_gateway_profiles.py: the regenerated discovery table rows (both WHO columns) and the sweep_omits validator (a frame of a WHO the profile does not advertise, or a frame other than *#4*0## / *#16*0*5##, is a violation). tests/test_services.py::test_handler_refuses_frame_only_on_an_explicit_true: the sweep_bus helper never hides a request on a handler without the gate or on a double returning a non-bool.
  • Fail-before / pass-after: 34 of the original 40 cases fail on the unpatched base; of the cases added for the first review, 15 fail on the branch before it; of this round's changes, 15 fail on the previous branch head (the MH201 sweep row, the table-vs-sweep check, the four F422 log-on-update cases, the humidity log case, the F455 interface case, the MH201 capture replay, the two test_gateway_profiles.py cases, and the four MH201 sweep expectations in test_multi_gateway.py, test_blank_start.py and test_multi_gateway_regressions.py); all pass with the change. test_issue_304_310_batch.py::test_discovery_asks_an_mh200_for_its_amplifiers is updated for the intended change; test_issue_674_f455_climate_trace.py (including test_gateway_supports_zone_status_property) passes unchanged.
  • Full suite against the PyPI-pinned OWNd behaviour (plain upstream OWNd master, f04d8aa, on PYTHONPATH): 4631 passed, 1 skipped, 100.0 % statement coverage of custom_components (16233 statements).
  • ruff check ., scripts/typing_ratchet.py (0 strict errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/manage_translations.py check: all pass.

Where are the traces

Trace PR: #799 (merged, ad8431ff); the MH201 captures are the issue_466 fixtures already in the repository.

Subsystem Capture Plant Frames verified by this PR
Startup requests refused by the gateway itself tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, run run1_oracle_replay_readonly F461 2.0.11 *#4*0##, *#16*0*5## (all 18 WHO 16 forms), *#5*0## NACK in under 10 ms, nothing on the event session
Per-zone dimension 0 on a forwarding path same archive, run run1_oracle_replay_readonly (plant_reads, rows 521-525) F461 2.0.11, zone 2 *#4*2*0## → *#4*2*0*0239## + ACK in 0.119 s; *#4*1*0## (unpopulated zone 1) NACK after 2.071 s (rows 374-376)
Bus-visible replies of the patched integration's sweep same archive, run run6_event_session_ha_fixed_integration (an event-session rx capture: it shows replies, not what HA did not send) F461 2.0.11 *#1*0##, *#2*0##, *#4*2##, *#4*2*60##, *#4*#0*20## answered on the event session; the omission of *#4*0## / *#16*0*5## / *#5*0## is verified by the HA debug log of the live retest above, not by this run
The same two refusals on a second gateway family tests/fixtures/traces/myhomeserver1_live_2026-10-24/myhome_trace_MyHomeServer1_live_2026-10-24.json, runs run1_identify, run2_reads_thermo_sound_alarm_lights and run3_reads_lights_addresses_humidity MyHomeServer1 3.87.13 *#4*0## NACK 51 ms (run 3); *#16*0*5##, *#16*101*5## NACK 3 ms (run 2); *#4*35## answered with 5 frames (run 2); zones 35..70 *#4*Z*0*TTTT## (run 1)
*#4*0## on a physical MH201 tests/fixtures/traces/issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json (frame 11) against myhome_trace_MH201_all_2026-09-25T19-05-42.json (frames 0-10) MH201 3.3.6 *#4*0## sent, no zone frame follows; zones 2, 3 and 4 report on the same plant
Per-zone temperature poll on MH201 / F450 / F454 / F459 / H4684 / MH200 / MH202, F422-routed zones no physical trace: oracle rows (below) and the #185 field report (MH201) — —

tests/test_startup_requests_per_firmware.py loads the first, fourth and fifth archives and replays their rows (see "How is it tested").


What does the Firmware Oracle / Encyclopedia say

  1. OpenWebNet Encyclopedia (functional/who-4-temperature-control/addressing.md, dimensions.md): WHERE 0 addresses the general probes ("all probes"); DIMENSION 0 "measured temperature" is read-only, four decimal digits 0000..0500. The status request *#4*0## is the general form of the zone status request; the encyclopedia does not say which gateway firmware forwards it.
  2. Firmware Oracle (published at results/mcp_index.json 53f9514):
    • *#4*0## (results/<P>/<V>/oracle/full/trace-corpus.tsv): nack, silent on F450 020010, F454 020051, F459 020105, F460 020012, F461 020011, H4684 020054, MH200 020102, MH202 010024, MyHomeServer1 028206; nack, out (forwarded) on F453 010100, F453AV 030014, MH200N 010108. That row plus the MH201 capture is GENERAL_ZONE_REQUEST_REFUSED.
    • *#4*1## (trace-corpus.tsv): the SCS gateways expand it into four bus queries, dimension 0, 12, 14 and 13 ($0499013000, $0499013012, $049901300E, $049901300D), nack, out on F450, F453, F453AV, F454, F459, H4684, MH200, MH200N and ack, out on MH202; F460 and F461 send a single $00499013000 and MyHomeServer1 one payload. Against a 99-zone central unit every query is answered (F454 + #0, tests/fixtures/traces/issue_629/home-assistant_myhome_2026-10-05T11-16-10.935Z.cleaned.log lines 489-700: *#4*1##..*#4*15## complete in ~1.3 s each with all five frames); a standalone thermostat answers only some, hence the NACK after 1.5-2 s. The 10 s socket hang of F455 multiple timeouts #674 was an F455 (not in the oracle) on actuator-only channels (*#4*Z*19*0*0##). That row puts F450 in the supports_zone_status exclusion tuple and keeps F460 out of ZONE_TEMPERATURE_POLL.
    • *#4*1*0## (results/<P>/<V>/oracle/full/thermo-ts10.tsv, also in tests/golden/firmware_oracle.json, the WHO 4 probe of scripts/update_gateway_profiles.py): ack, out on MH202 010024; nack, out with the bus payload 93 01 14 00 / $004990001000 on the other eleven images (F450, F453, F453AV, F454, F459, F460, F461, H4684, MH200, MH200N, MyHomeServer1). The per-zone dimension 0 request this PR sends is forwarded everywhere; the NACK is the bus TX timeout of an image with no thermostat wired, identical to the live F461's unpopulated zone 1 (rows 374-376), while its populated zone 2 answers with an ACK in 119 ms. Until a command-session capture of *#4*Z*0## on an SCS-only gateway (F454 / MH200) exists, no _poll_answered callback is attached to it.
    • *#16*0*5## (results/<P>/<V>/oracle/full/sound-who16-source.tsv): nack, silent on F450, F459, F460, F461, MH202, MyHomeServer1 (77 of 77 WHO 16 frames silent on each); ack, out on F453, F453AV, F454, H4684, MH200, MH200N. That row is SOUND_STATUS_REQUEST_REFUSED.
    • Open, not published: feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60 (thermo-zone-requests suite) adds zone 2, #0 / #23#1 and #4#01 coverage: *#4*2*0## out on every image (consistent with the thermo-ts10.tsv row above), *#4*2#4#01*0## nack, silent on F450, F453, F453AV, F454, F459, F460, F461, MH200N, MH202 and MyHomeServer1 and forwarded on H4684 and MH200, *#4*2#4#01## a thermoregulation-daemon crash on the MH202. That is INTERFACE_ZONE_REQUEST_REFUSED; the gate fails closed (no poll) on the listed models and sends on the two that forward it and on the MH201, which is not in the oracle.

Review follow-up (review 5642254816)

  1. F450 joins the supports_zone_status exclusion tuple, F460 leaves ZONE_TEMPERATURE_POLL (now explicit) and the SCS-only list in climate.md; runtime_behaviour.md line 15 says which models poll *#4*<WHERE>## and which ask *#4*<WHERE>*0##; ("F450", True) and ("F460", False) added to the zone temperature poll table -> ede6721
  2. F422 interface-routed zones: interface_zone_request_refused (profile flag first, INTERFACE_ZONE_REQUEST_REFUSED fallback), _polls_temperature off for such zones on those models with one DEBUG line, the _poll_answered omission commented, climate.md qualified, tests -> 329ec0a
  3. myhome.sweep_bus with force=False skips handler._refused_by_gateway(q) frames, force=True sends them; F461 test both ways, MH202 multi-gateway expectation 45 -> 43, services.yaml / services.md / release notes -> 06afedf
  4. HL4684 keyed as H4684 in _model_key and listed in the supports_zone_status tuple; the OWNd-aliased spellings (L4684, NT4684, item numbers) follow their profile's model name in supports_zone_status (found while cross-checking the diff); tests -> 8dcd9bc
  5. Day 0 matrix WHO 4 / WHO 16 rows per gateway, GATEWAY_DISCOVERY_METADATA notes for MH202 (no more "Sound (*#16*0*5##)"), F454, F461, MyHomeServer1, MH200, hardware-compatibility.md regenerated -> a0ff59a
  6. PR body: thermo-ts10.tsv row *#4*1*0## cited directly, feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60 kept for zone 2 / #0 / #4#01, the draft blocker sentence replaced, the state diagram updated for the interface gate and sweep_bus.

(The shas are those after the rebase on the current v2-phase1-architecture.)

Review follow-up (review 5642844379)

  1. "MH201" added to GENERAL_ZONE_REQUEST_REFUSED on the issue_466 evidence (sweep frame 11 *#4*0## with no zone frame, sibling trace zones 2, 3, 4); runtime_behaviour.md lines 15 / 37, climate.md, the b16 release note and ("MH201", False, True) in test_sweep_skips_requests_the_gateway_refuses; the tests whose default sweep model is an MH201 drop *#4*0## (the delegated-WHO 4 follower case moves to an MH200N) -> e4d8adc
  2. climate.py comments corrected: SCS gateways expand *#4*Z## into the four dimension queries (F454 + #0 answers in ~1.3 s, issue_629; the F455 multiple timeouts #674 10 s hang was an F455 on actuator-only channels); the *#4*1*0## NACK is the unpopulated-zone bus TX timeout (live F461 zone 1 after 2.07 s) while the populated zone 2 answers in 119 ms; _poll_answered stays off "until a command-session capture of *#4*Z*0## on an SCS-only gateway exists"; *#4*Z*0## kept; feat(climate): hydrate cold subordinate zones via one-shot *#4*Z## once live #0 central unit is confirmed #813 named as the cold-start #0 track; this body corrected the same way -> 3c2f5c9
  3. _polls_temperature is a pure boolean property; the one-shot DEBUG line and _interface_poll_skip_logged moved into async_update, written when the poll is withheld (also when a fan / humidity request keeps the update going); tests assert nothing is logged by construction or by the property and once on update -> a4abfe6
  4. hardware-compatibility.md: the column is split rather than trimmed, because calibrate_discovery_with_ownd recomputes queried_whos from the OWNd profile and the oracle validator requires the two to match; column 2 stays the profile's WHOs, column 3 is what the sweep sends (per-row sweep_omits, validated, and checked against _discovery_frames() by a test); MH200 and MH200N split, the MH201 note (*#4*0## refusal + per-zone *#4*<WHERE>*0##) and the H4684 / HL4684 row added; regenerated with sync_documentation.py --update -> 2af4e8b
  5. services.md: myhome.sweep_bus with force: false sends zero WHO 4 frames on F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1 and triggers no per-zone poll; force: true sends them and the firmware drops them -> da8c29b
  6. tests/test_startup_requests_per_firmware.py loads f461_live_2026-10-23 (zone 2 *#4*2*0*0239##, zone 1 NACK), myhomeserver1_live_2026-10-24 (zones 35..70) and the issue_466 MH201 sweep (frame 11 *#4*0## with no reply) with the loaders the other trace tests use and replays the captured frames; the synthetic zone 3 cases stay and say so; the "Where are the traces" row of run6 credits it with the bus-visible replies only and the tx omissions are attributed to the HA debug log of the live retest -> d3be161 + this body

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. (The two skips: live F461, MyHomeServer1 and the physical MH201 sweep plus published rows; the *#4*Z*0## fallback: the published thermo-ts10.tsv row and the live F461; the F422 interface gate: feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60, unpublished, failing closed.)
  • 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.

🤖 Generated with Claude Code

@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 marked this pull request as draft October 24, 2026 13:39
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
… poll of OpenWebNet-HA#794

The merged OpenWebNet-HA#793 and OpenWebNet-HA#794 both act on the F454 / MH202 zone update: OpenWebNet-HA#794 adds the
*michnovka#4*Z*0## temperature request on SCS-only gateways, OpenWebNet-HA#793 keeps *michnovka#4*Z*60## out.
The test now asserts the humidity frame is absent instead of an empty send list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from 911c77d to d4284e4 Compare October 24, 2026 15:54
@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from d4284e4 to 5dbb70a Compare October 24, 2026 18:07
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck please rebase and solve conflcts

@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from 5dbb70a to f1b1d9e Compare October 24, 2026 19:27
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Rebased on the current base (head f1b1d9e, mergeable): climate.py keeps both the merged #791 setpoint-in-force logic and this PR's zone temperature poll / restored flag, the release-notes line sits below the merged #791 entry, and the Known Limitations row from #791 came in untouched. Tests of both PRs run together on this branch (64 passed) and the full suite is 4538 passed at 100 % coverage; ruff, typing ratchet, HA standards and docs sync are clean. The other open drafts of the batch were rebased the same way.

@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from f1b1d9e to 9b9b12a Compare October 24, 2026 21:10

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

Solid protocol direction — verified GENERAL_ZONE_REQUEST_REFUSED and SOUND_STATUS_REQUEST_REFUSED 1:1 against own-firmware-oracle (trace-corpus.tsv, sound-who16-source.tsv, thermo-ts10.tsv), the live F461 (2.0.11) and MyHomeServer1 (3.87.13) captures, and OWNd==2.0.0b11.

Requesting changes for a few concrete issues before undrafting/merging (inline comments attached, plus two cross-file items outside the diff hunks below):

  1. F450 / F460 mismatch (gateway.py, climate.md, test_startup_requests_per_firmware.py): Neither "F450" nor "F460" is in supports_zone_status's exclusion list, so zone_temperature_poll_needed returns False for both — causing legacy SCS gateway F450 (020010) to send *#4*<WHERE>## (#674 timeout hazard) instead of *#4*<WHERE>*0##, while climate.md misclassifies F461-sibling F460 (020012) as SCS-only.
  2. F422 interface-routed zones (climate.py): *#4*2#4#01*0## is nack, silent (dropped locally by firmware) on F450, F454, F459, and MH202 in OpenWebNet-HA/own-firmware-oracle#60.
  3. myhome.sweep_bus (custom_components/myhome/services.py:241-270): When force=False, sweep_bus builds its own general_queries list and gates only on _handler_supports_who(handler, who) without checking handler._refused_by_gateway(q). As a result, myhome.sweep_bus (force=False) still sends refused *#4*0## and *#16*0*5## frames and waits BUS_QUIET_PERIOD per NACKed frame in send_paced(). Please filter general by not handler._refused_by_gateway(q) when not force (or reuse handler._discovery_frames()) and add a test for force=False vs force=True.
  4. Published oracle row for *#4*Z*0## (Draft blocker in PR description): The PR body states there is no published *#4*Z*0## row yet, blocking on own-firmware-oracle#60. Note that *#4*1*0## is already published across all 12 emulated gateways in results/<P>/<V>/oracle/full/thermo-ts10.tsv (and already in tests/golden/firmware_oracle.json, checked by scripts/update_gateway_profiles.py:502): ack, out (93 01 14 00) on MH202 and nack, out (93 01 14 00 / $004990001000\r / internal bus payload) on the other 11 models. You can cite thermo-ts10.tsv directly alongside #60 (which adds zone 2, #0/#23#1, and #4#01 coverage), so this PR doesn't need to wait on #60 once the items here are addressed.

Comment on lines +390 to +397
if self.supports_zone_status:
return False
supported = getattr(getattr(self.gateway, "profile", None), "supports_general_heating_status", None)
if isinstance(supported, bool):
# The OWNd profile wins when it states the flag: a sweep the gateway answers makes
# the per-zone poll pointless, whatever the fallback table says.
return not supported
return self._model_key in ZONE_TEMPERATURE_POLL

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.

Bug (F450) & code/docs mismatch (F460): supports_zone_status above (lines 342–359) checks ("F455", "F454", "MH200", "MH201", "MH202", "F452", "F453", "F458", "F459", "H4684") — neither "F450" nor "F460" is in that tuple, so supports_zone_status returns True for both and zone_temperature_poll_needed returns False for both despite "F450" and "F460" being in ZONE_TEMPERATURE_POLL:

  • F450 (020010): Legacy 4-module DIN SCS gateway whose *#4*1## oracle behavior in trace-corpus.tsv is identical to F454 / F459 / H4684 / MH200 (4 raw SCS dimension telegrams 93 01 14 00/0C/0E/0D + NACK, hitting the F455 multiple timeouts #674 10 s command-session timeout per standalone thermostat). Skipping *#4*0## on F450 while leaving "F450" out of supports_zone_status makes climate.py send *#4*<WHERE>## instead of *#4*<WHERE>*0##. "F450" should be added to supports_zone_status's exclusion tuple.
  • F460 (020012): Sibling of F461 (020012) that expands *#4*1## via $00499013000\r just like F461. Keeping supports_zone_status == True (zone_temperature_poll_needed == False) matches F461, so F460 should be removed from the SCS-only list in docs/configuration/climate.md (and either dropped from ZONE_TEMPERATURE_POLL or noted as gated out by supports_zone_status).

Comment thread custom_components/myhome/gateway.py Outdated
Comment on lines +362 to +366
def _model_key(self) -> str:
"""The gateway model the request tables are keyed on (``F461``, ``MYHOMESERVER1``)."""
profile_model = getattr(getattr(self.gateway, "profile", None), "model_name", None)
name = profile_model if isinstance(profile_model, str) and profile_model != "Generic" else self.model
return "".join(character for character in str(name).upper() if character.isalnum())

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.

Edge case (HL4684 SSDP model string): manifest.json registers SSDP modelName: "HL4684" (authoritative=True in identity.py, and OWNd==2.0.0b11 _PROFILE_ALIASES aliases h4684/l4684/n4684/nt4684/lgrh4684 rather than hl4684, resolving to GenericGatewayProfile). For model="HL4684", "H4684" in norm in supports_zone_status is False and _model_key returns "HL4684", missing "H4684" in both GENERAL_ZONE_REQUEST_REFUSED and ZONE_TEMPERATURE_POLL. Normalizing "HL4684" -> "H4684" here (and including "HL4684" in supports_zone_status) closes that gap until OWNd aliases hl4684.

Comment on lines +578 to +581
elif self._polls_temperature:
# The dimension 0 request goes out on every gateway of the firmware oracle; the
# dimensionless *#4*Z## times out on these (#674) and *#4*0## is refused.
await self._gateway_handler.send_status_request(OWNHeatingCommand.get_temperature(self._full_where))

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.

Two protocol nuances on this branch:

  1. F422 interface-routed zones (Z#4#II): When self._interface is not None, self._full_where is f"{self._where}#4#{self._interface}" (e.g. 2#4#01). Per OpenWebNet-HA/own-firmware-oracle#60 (*#4*2#4#01*0##), that frame is nack, silent (dropped locally without reaching the bus) on F450, F454, F459, and MH202 (forwarded only on H4684 and MH200). Consider gating _polls_temperature off when self._interface is not None on gateways whose firmware drops #4#II WHO 4 dimension requests (or qualifying this comment and climate.md).
  2. Intentional omission of _poll_answered: Unlike *#4*Z## above, *#4*Z*0## terminates with NACK on the command session across 11 of 12 oracle gateways even when forwarded to the SCS bus (thermo-ts10.tsv), which cancels written in CommandWorkerPool. Not attaching _poll_answered here is therefore the right call so healthy SCS zones are not falsely flagged unresponsive — worth a brief comment so nobody "fixes" it later.

Comment thread docs/configuration/climate.md Outdated
* **Target Temperature**: Adjustable setpoint slider in 0.5 °C increments.
The slider shows the setpoint the thermostat regulates to (dimension 12, knob offset included) whenever the zone has reported it; the commanded setpoint (dimension 14) is only shown until then. On an F461 the two can differ (21.0 commanded, 23.5 in force on a live capture), and the attribute `local_target_temperature` carries the same value. When the zone leaves `off`, the value reported while it was off is taken as the protection setpoint (#383), so the commanded setpoint is shown until the zone reports dimension 12 again; after a `set_temperature` call the next report, dimension 12 or 14, is shown.
* **Current Temperature**: Real-time ambient reading reported by the probe.
On SCS-only gateways whose firmware drops the general request `*#4*0##` (F450, F454, F459, F460, H4684, MH200, MH202) and on the MH201, each zone asks for its own temperature with `*#4*<WHERE>*0##` after the startup sweep, so a standalone thermostat is not `unknown` until it next reports (#185). After a restart the last reading is shown with the attribute `current_temperature_restored: true` until the zone reports again.

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.

Per the gateway.py comment, F460 (020012, F461 sibling) has supports_zone_status == True and polls *#4*<WHERE>## rather than *#4*<WHERE>*0##, so F460 should be dropped from this SCS-only list (and F450 added to supports_zone_status's exclusion list so F450 actually takes the *#4*<WHERE>*0## path).

Comment thread docs/configuration/runtime_behaviour.md Outdated
Comment on lines +15 to +17
| `*#4*0##` | Thermoregulation | All gateways except F450, F454, F459, F460, F461, H4684, MH200, MH202 and MyHomeServer1, whose firmware drops it (firmware oracle); their SCS-only zones ask `*#4*<WHERE>*0##` instead |
| `*#5*0##` | Burglar alarm | MH202, H4890 |
| `*#16*0*5##` | Sound system (dimension 5; lists every amplifier) | F454, H4890, MH200, MH201, MH202, MyHomeServer1 |
| `*#16*0*5##` | Sound system (dimension 5; lists every amplifier) | Gateways whose profile advertises WHO 16, except F450, F459, F460, F461, MH202 and MyHomeServer1, whose firmware drops it (firmware oracle) |

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.

Two small doc follow-ups nearby:

  • In line 15, F460 polls *#4*<WHERE>## rather than *#4*<WHERE>*0## (supports_zone_status == True, same 020012 translator family as F461), so "their SCS-only zones ask *#4*<WHERE>*0## instead" is subtle when F460, F461, and MyHomeServer1 are in the same list — worth clarifying "while F460, F461 and MyHomeServer1 poll *#4*<WHERE>## per zone and the SCS-only models ask *#4*<WHERE>*0##".
  • Just below in the Day 0 Blank-Start Discovery Matrix (lines 37 & 39), WHO 4 and WHO 16 still say ✅ Yes (*#4*0##) and ✅ Yes (*#16*0*5##) unconditionally (and GATEWAY_DISCOVERY_METADATA["MH202"]["notes"] in scripts/update_gateway_profiles.py / hardware-compatibility.md still says Full support including ... Sound (*#16*0*5##)). On gateways that drop *#4*0## / *#16*0*5##, blank-start discovery with no registry entries or myhome.yaml happens on first bus traffic rather than from the startup sweep.

Comment on lines +105 to +114
@pytest.mark.parametrize(
("model", "expected"),
[
("MH201", True), ("MH202", True), ("F454", True), ("MH200", True), ("H4684", True), ("F459", True),
("F455", False), ("F453", False), ("MH200N", False), # unknown, or the sweep answers
("F461", False), ("MyHomeServer1", False), ("Generic", False), # polled with *#4*Z##
],
)
def test_which_gateways_need_a_zone_temperature_poll(hass, model, expected):
assert _handler(hass, model).zone_temperature_poll_needed is expected

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.

test_sweep_skips_requests_the_gateway_refuses above parametrizes both "F450" and "F460", whereas this test omits both — which hid F450 returning False here (supports_zone_status missing "F450"). Please add ("F450", True) and ("F460", False) (plus "HL4684" if normalized).

@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from 9b9b12a to 5985b69 Compare October 25, 2026 10:03
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough pass; every item is on the branch (head 5985b69, rebased on the current base, one commit per point):

  1. F450 / F460 (768f29b): "F450" is in the supports_zone_status exclusion tuple, so it polls *#4*Z*0## (its *#4*1## row equals the F454's: four raw SCS telegrams and a NACK). F460 keeps supports_zone_status True and leaves ZONE_TEMPERATURE_POLL, which is now an explicit table of the SCS-only models (F450, F454, F459, H4684, MH200, MH201, MH202), and the SCS-only list in climate.md; runtime_behaviour.md line 15 reads as you suggested. ("F450", True) and ("F460", False) are in the poll-table parametrization.
  2. F422 interface-routed zones (1c5c651): new handler property interface_zone_request_refused, read from the profile first (getattr(profile, "supports_interface_zone_requests", None); that flag belongs in OWNd) with a MyHOME fallback table of the images where feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60 has *#4*2#4#01*0## nack / silent. _polls_temperature is off for a zone with an interface on those gateways and logs once at DEBUG; H4684, MH200 and MH201 poll *#4*Z#4#01*0## as before. The async_update comment and climate.md are qualified, and the _poll_answered omission is now commented with the thermo-ts10.tsv reason (*#4*Z*0## ends in NACK on 11 of 12 even when forwarded, which cancels written).
  3. myhome.sweep_bus (c9e1c3e): the general requests go through handler._refused_by_gateway(q) when force is False and are all sent when force is True, on the primary and follower lists alike. Tested on an F461 both ways (force=False → *#1*0##, *#2*0## only; force=True → all five); the MH202 expectation in test_multi_gateway.py goes 45 → 43 status requests; services.yaml / services.md describe force.
  4. HL4684 (fec46c5): _model_key keys HL4684 as H4684 and "HL4684" is in the supports_zone_status tuple. While checking this, the spellings OWNd aliases (L4684, NT4684, item numbers such as 003598) had the same gap in supports_zone_status; it now also judges get_gateway_profile(model).model_name, with tests for each.
  5. Docs (5985b69): the Day 0 matrix WHO 4 / WHO 16 rows are per gateway with the first-bus-frame note; GATEWAY_DISCOVERY_METADATA no longer claims "Sound (*#16*0*5##)" for the MH202 (77 / 77 WHO 16 inputs silent in sound-who16-source.tsv) and notes the dropped requests on F454, F461, MyHomeServer1 and MH200; hardware-compatibility.md regenerated.
  6. Oracle citation: the body cites the published thermo-ts10.tsv row *#4*1*0## directly (ack / out on MH202; nack / out with 93 01 14 00 / $004990001000 on the other 11) and keeps unable to see Automation (cover) events #60 only for zone 2, #0 and #4#01, so the PR no longer waits on it. I will un-draft once CI on this head is green.

Full suite 4584 passed / 1 skipped at 100 % coverage; ruff, typing ratchet, HA standards, docs sync and translations clean; tests/test_startup_requests_per_firmware.py 60 cases (15 of the 20 new ones fail on the previous head).

@gdluck
gdluck marked this pull request as ready for review October 25, 2026 10:21
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

All checks green on 5985b69; marking ready for review.

@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 working through the items from review 5642254816 (F450 / F460 split, HL4684 + OWNd aliases, F422 Z#4#II guard, sweep_bus(force=...), current_temperature_restored).

Cross-checking 5985b696 against both the existing physical traces in tests/fixtures/ and the newly ingested live captures (#799) leaves six concrete items to resolve before merge (inline below):

  1. MH201 double-queries *#4*0## + *#4*Z*0## and overlooks two committed physical MH201 traces (gateway.py:158-171, runtime_behaviour.md:15/37, 2.0.0b16.md).
  2. Keep *#4*Z*0## as the SCS-only startup baseline here, but fix the inaccurate protocol comments & PR body claims (climate.py:576-580, climate.py:597-604) that conflate #674's F455 10 s socket timeout and the busless QEMU oracle's NACK with live F454 + #0 (issue_629) and populated *#4*Z*0## (f461_live_2026-10-23). Cold-start #0 subordinate-zone setpoint hydration is tracked separately in #813 so this PR stays focused.
  3. Make _polls_temperature a pure property (climate.py:397, 412-433) — right now it mutates _interface_poll_skip_logged and logs DEBUG during __init__ before the entity is attached to hass or the registry.
  4. Resolve the self-contradiction in docs/getting-started/hardware-compatibility.md (L55-66) / scripts/update_gateway_profiles.py (L183-236), where Column 2 (Initial Discovery Subsystems Queried) still lists 4 and 16 on models whose Column 3 note says the sweep omits them, and add the missing MH201 / HL4684 notes.
  5. Document in docs/configuration/services.md (L139-145) that myhome.sweep_bus (force: false) now sends zero WHO 4 frames on the gateways refusing *#4*0## (and does not trigger per-zone polls).
  6. Back tests/test_startup_requests_per_firmware.py with real fixture frames (it currently loads zero trace files and uses synthetic Zone 3 frames despite the PR body stating trace frames are inlined there), and tighten the PR body's run6_event_session_ha_fixed_integration table entry (run6 is an evt-only rx capture and cannot by itself prove tx omissions).

(Separate trace-corpus schema, timestamp causality, issue_674 forensics, and fixture privacy-gate follow-ups are tracked in #814.)

Comment on lines +158 to +171
GENERAL_ZONE_REQUEST_REFUSED = frozenset(
{"F450", "F454", "F459", "F460", "F461", "H4684", "MH200", "MH202", "MYHOMESERVER1"}
) # *#4*0## (trace-corpus.tsv); F453, F453AV and MH200N forward it
SOUND_STATUS_REQUEST_REFUSED = frozenset(
{"F450", "F459", "F460", "F461", "MH202", "MYHOMESERVER1"}
) # *#16*0*5## (sound-who16-source.tsv)
# Zones of these SCS-only gateways report nothing after the startup sweep, so each zone asks
# for its temperature (*#4*Z*0##): the ones refusing *#4*0## that also forward *#4*Z## to the
# bus unanswered (#674), and the MH201, whose standalone thermostats stayed unknown after every
# restart (#185; the MH201 is not in the oracle). F460, F461 and MyHomeServer1 refuse *#4*0##
# too but answer *#4*Z## themselves (F460 and F461 expand it into the four dimension requests,
# $00499013000 in trace-corpus.tsv; MyHomeServer1 replies from its cache, #649), so their zones
# keep the zone status poll and are not listed here.
ZONE_TEMPERATURE_POLL = frozenset({"F450", "F454", "F459", "H4684", "MH200", "MH201", "MH202"})

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.

MH201 sends both *#4*0## and per-zone *#4*Z*0##, and two physical MH201 traces already exist in the repo.

MH201 is in ZONE_TEMPERATURE_POLL (L171) and excluded from supports_zone_status, yet omitted from GENERAL_ZONE_REQUEST_REFUSED (L158-160). Consequently, every startup on an MH201 (1 command worker, 100 ms pacing) still transmits *#4*0##, waits BUS_QUIET_PERIOD (0.75 s) on silence/NACK, and then also queues *#4*Z*0## for every zone.

Furthermore, we do have physical MH201 evidence in tests/fixtures/ beyond the #185 text report:

  1. tests/fixtures/traces/issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json: frame[11] sent tx *#4*0## on a physical MH201 (fw 3.3.6, WHO 13 code 200) and received zero WHO 4 frames, whereas sibling trace myhome_trace_MH201_all_2026-09-25T19-05-42.json proves active WHO 4 zones 2, 3, 4 on that exact plant (*#4*2*0*0199##, *#4*3*0*0215##, *#4*4*0*0181##).
  2. tests/fixtures/traces/mh201-fresh-discovery-20260930.log + tests/fixtures/plants/mh201_physical_plant/ (MH201 fw 3.6.27, Linux bt_termo cousin of F454 / MH202, also zero WHO 4 replies during initial discovery).

Please add "MH201" to GENERAL_ZONE_REQUEST_REFUSED, update the corresponding lists in runtime_behaviour.md (L15, L37), .github/release-notes/2.0.0b16.md, and the PR body, and add ("MH201", False, True) to test_sweep_skips_requests_the_gateway_refuses.

Comment thread custom_components/myhome/climate.py Outdated
Comment on lines +412 to +430
def _polls_temperature(self) -> bool:
"""Whether this zone asks for its temperature (*#4*Z*0##) on an SCS-only gateway (#185)."""
gateway = getattr(self, "_gateway_handler", None)
if self._central or getattr(gateway, "zone_temperature_poll_needed", False) is not True:
return False
if self._interface is not None and getattr(gateway, "interface_zone_request_refused", False) is True:
# The firmware drops *#4*Z#4#II*0## before the bus on these gateways (own-firmware-oracle
# PR #60: nack/silent on F450, F454, F459 and MH202 among the SCS-only models; the MH202
# even crashes on *#4*Z#4#II##), so the zone waits for the bus to report instead.
if not self._interface_poll_skip_logged:
LOGGER.debug(
"%s %s is behind interface %s and %s firmware drops `*#4*%s*0##`; waiting for the zone to report",
self._gateway_handler.log_id,
self._display_name,
self._interface,
self._gateway_handler.model,
self._full_where,
)
self._interface_poll_skip_logged = True

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.

Keep _polls_temperature a pure boolean @property and move the one-shot debug log to async_update().

MyHOMEClimate.__init__ reads self._polls_temperature at L397 before the entity is registered or attached to hass (entity_id / registry friendly name are not set yet). Mutating self._interface_poll_skip_logged = True and calling LOGGER.debug(...) inside the property means the one-shot log fires during object construction (including transient/rejected discovery instances) and never fires when async_update() actually skips the poll.

Comment thread custom_components/myhome/climate.py Outdated
Comment on lines +597 to +604
# The plain-zone dimension 0 request goes out on every gateway of the firmware oracle
# (thermo-ts10.tsv); the dimensionless *#4*Z## times out on these (#674) and *#4*0## is
# refused. A zone behind an F422 is only polled where the firmware forwards Z#4#II
# requests (_polls_temperature).
# No _poll_answered callback on purpose: *#4*Z*0## ends with a NACK on the command
# session on 11 of the 12 oracle gateways even when it was forwarded to the bus
# (thermo-ts10.tsv: nack/out everywhere but the MH202), which cancels `written`, so the
# callback would count a healthy SCS zone as unresponsive.

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.

Keep *#4*Z*0## here, but correct the inaccurate protocol explanations at L573-580 and L597-604 (and in the PR body).

Keeping SCS-only startup polling on *#4*Z*0## in this PR is the right call (fast ~120 ms/zone, safe on single-worker MH200/MH201 and F422, while RestoreEntity + *#4*#0## cover warm restarts; cold-start #0 subordinate-zone setpoint hydration is tracked in #813). However, two claims in these comments contradict the Firmware Oracle and committed physical traces:

  1. *#4*Z## (L573-578, L598): Emulated SCS gateways (F450, F453, F453AV, F454, F459, H4684, MH200, MH200N, MH202) do expand *#4*Z## onto the SCS bus into the 4 dimension queries ($0499ZZ3000, 12, 0E, 0D in trace-corpus.tsv) and close the command session cleanly (ACK on MH202, ~1.5-2.0 s NACK when unanswered on the other 8). On F454 + 99-zone central unit #0 (tests/fixtures/traces/issue_629/home-assistant_myhome_2026-10-05T11-16-10.935Z.cleaned.log, L489-700), *#4*1##..*#4*15## all succeeded in ~1.3 s with all 5 frames (dim 0, 12, mode, 14, 13). The 10 s TCP socket hang in #674 occurred on F455 (not in the oracle) on actuator-only channels (*#4*Z*19*0*0##) where nothing answered dim 12/14/13, and standalone thermostats do not answer all four dimensions.
  2. *#4*Z*0## NACK rationale (L601-604): *#4*1*0## ended in NACK on 11/12 oracle images because the QEMU oracle runs without a physical thermostat wired to the bus (bus TX timeout -> *#*0##, identical to unpopulated Zone 1 in f461_live_2026-10-23 row[374-376] NACKing after 2.07 s). On a populated live zone (f461_live_2026-10-23 row[521-523], *#4*2*0##), the gateway returns *#4*2*0*0239## + *#*1## ACK in 119 ms. Omitting _poll_answered until we have a command-session capture of *#4*Z*0## on an SCS-only gateway (F454/MH200) is sensible — please phrase the comment that way rather than stating that forwarded *#4*Z*0## NACKs on live zones.

| :--- | :---: | :---: | :--- | :--- |
| `gateway` | string | No | Target gateway MAC address (defaults to all gateways). | `"00:03:50:20:00:01"` |
| `force` | boolean | No | Force probe across all subsystems regardless of gateway profile restrictions (defaults to `false`). | `true` |
| `force` | boolean | No | Force probe across all subsystems regardless of gateway profile restrictions, including the general requests the gateway firmware drops (`*#4*0##`, `*#16*0*5##` on most models; see [runtime behaviour](runtime_behaviour.md)). Defaults to `false`, which sends the same general requests as the startup sweep. | `true` |

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.

Document that default myhome.sweep_bus (force: false) emits zero WHO 4 traffic on these gateways.

Line 139 above says myhome.sweep_bus "Actively queries status across all configured subsystems (lighting, automation, thermoregulation, and gateway diagnostics). It is used to refresh entity states...". With force: false on F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202, and MyHomeServer1, sweep_bus skips *#4*0## and does not trigger per-zone *#4*Z## / *#4*Z*0## polls — so zero WHO 4 frames are sent (and force: true still gets dropped by firmware before reaching the bus). Please note this explicitly at L139-145.

Comment on lines +57 to +65
| **MH202** | WHO 1, 2, 4, 5, 16, 18 | Burglar Alarm (`*#5*0##`) and Energy (`*#18*51*...`) queries. WHO 4 and WHO 16 are advertised, but the `010024` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle), so the sweep leaves both out and each climate zone asks `*#4*<WHERE>*0##` itself. |
| **H4890 / AM4890** | WHO 1, 2, 4, 5, 16, 18 | Supports Burglar Alarm, Sound (`*#16*0*5##`), and Energy meter 51 queries. |
| **F454** | WHO 1, 2, 4, 16, 18 | Full lighting, cover, climate, sound (`bt_difson` firmware-verified), and energy meter 51 queries. Alarm query omitted. |
| **F454** | WHO 1, 2, 4, 16, 18 | Full lighting, cover, sound (`bt_difson` firmware-verified), and energy meter 51 queries. The `020051` firmware drops `*#4*0##` (own-firmware-oracle), so each climate zone asks `*#4*<WHERE>*0##` itself. Alarm query omitted. |
| **F452 / F453 / F453AV** | WHO 1, 2, 4, 16, 18 | Lighting, cover, climate, sound (`bt_difson` firmware-verified on F453/F453AV), and energy meter 51 queries. Alarm query omitted. |
| **F461** | WHO 1, 2, 4, 16, 18 | Lighting, cover, climate, sound (unmeasured class default), and energy meter 51 queries. Alarm query omitted. |
| **F461** | WHO 1, 2, 4, 16, 18 | Lighting, cover, and energy meter 51 queries. The `020011` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle, confirmed live on 2.0.11), so the sweep leaves both out; zones are polled with `*#4*<WHERE>##`. Alarm query omitted. |
| **F455** | WHO 1, 2, 4, 18 | Entry-level gateway: lighting, cover, climate, and energy meter 51 queries. Sound (WHO 16/22) and Alarm (WHO 5) omitted per installer manual. |
| **MyHomeServer1** | WHO 1, 2, 4, 16, 18 | Lighting, cover, climate, sound (unmeasured class default), and energy meter 51 queries. Alarm query omitted. |
| **MyHomeServer1** | WHO 1, 2, 4, 16, 18 | Lighting, cover, and energy meter 51 queries. The firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle `028206`, confirmed live on 3.87.13), so the sweep leaves both out; zones are polled with `*#4*<WHERE>##`. Alarm query omitted. |
| **MH201** | WHO 1, 2, 4, 16, 18 | Lighting, cover, climate, sound (unmeasured class default), and energy meter 51 queries. Alarm query omitted. |
| **MH200 / MH200N** | WHO 1, 2, 4, 16 | Lighting, cover, climate, and sound queries (`bt_difson` hardware and firmware-verified on both). Alarm and energy queries omitted. |
| **MH200 / MH200N** | WHO 1, 2, 4, 16 | Lighting, cover, climate, and sound queries (`bt_difson` hardware and firmware-verified on both). The MH200 `020102` firmware drops `*#4*0##` (own-firmware-oracle; the MH200N forwards it), so its climate zones ask `*#4*<WHERE>*0##` themselves. Alarm and energy queries omitted. |

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.

Column 2 (Initial Discovery Subsystems Queried) now contradicts Column 3 (Profile Support Notes), and MH201 / HL4684 are incomplete/missing.

  1. Column 2 is titled Initial Discovery Subsystems Queried, yet still lists 4 and 16 for MH202, F461, and MyHomeServer1 (and 4 for F454 and MH200) while Column 3 states "so the sweep leaves both out". Please update scripts/update_gateway_profiles.py (GATEWAY_DISCOVERY_METADATA + its validator) so Column 2 reflects the WHOs actually queried during initial discovery (or rename/split advertised profile WHOs vs sweep-queried WHOs; note MH200 vs MH200N also differ on WHO 4).
  2. MH201 (L64) omits both its *#4*0## refusal (issue_466 physical sweep trace) and its per-zone *#4*<WHERE>*0## poll (contradicting runtime_behaviour.md:15 and climate.md:59).
  3. HL4684 / H4684 is present in Supported Gateways (L22) and GENERAL_ZONE_REQUEST_REFUSED / ZONE_TEMPERATURE_POLL, but missing from Gateway Startup Discovery Capabilities.

Comment on lines +249 to +259
async def test_current_temperature_is_restored_and_flagged_until_the_bus_reports(hass):
zone = _zone(hass, _handler(hass, "MH202"))
await zone.async_restore_last_state(
State("climate.zone_3", "heat", {"temperature": 21.0, "current_temperature": 23.9})
)
assert zone.current_temperature == 23.9
assert zone.extra_state_attributes["current_temperature_restored"] is True
zone.handle_event(OWNHeatingEvent("*#4*3*0*0241##"))
assert zone.current_temperature == 24.1
assert "current_temperature_restored" not in zone.extra_state_attributes

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.

Load and replay actual captured frames from the trace fixtures, and align the PR body's trace claims.

  1. Under ## Where are the traces, the PR body claims "The frames are also inlined in tests/test_startup_requests_per_firmware.py." Currently this file loads zero fixture files, mocks send_status_request = AsyncMock(), and synthesizes Zone 3 frames (*#4*3*0*0215##, *#4*3*0*0241##), whereas f461_live_2026-10-23 has Zone 2 (*#4*2*0*0239##) and myhomeserver1_live_2026-10-24 has Zones 35..70. Please add fixture-backed assertions loading the captured rows/frames from f461_live_2026-10-23, myhomeserver1_live_2026-10-24, and issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json.
  2. In the PR body's Where are the traces table, row 2 cites run6_event_session_ha_fixed_integration for "the sweep of the patched integration without *#4*0## / *#16*0*5##". Because run6 is an evt-only passive rx capture (0 tx rows, 0 cmd rows), gateway-dropped requests (*#4*0##, *#5*0##, *#16*0*5##), NACKed *#4*2*11##, and unanswered *#18*51*...## produce zero evt frames whether HA sent them or not — please credit run6 only with the bus-visible replies (*#1*0##, *#2*0##, *#4*2##, *#4*2*60##, *#4*#0*20##) and attribute tx-skip verification to the HA debug log from the live retest.

@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from 5985b69 to d3be161 Compare October 25, 2026 11:35
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks for the second pass; all six are on the branch (head d3be161, rebased on 089fef5, one commit per item).

  1. MH201 (e4d8adc): added to GENERAL_ZONE_REQUEST_REFUSED on the issue_466 evidence: sweep frame 11 is *#4*0## and no zone frame follows it (only *#4*0#0*17## three times and zone 105's dimension 15), while the sibling trace shows zones 2, 3 and 4 (*#4*4*0*0241##, *#4*2*14*0100*3##, *#4*3*12*0090*3##; the three *#4*Z*0*TTTT## frames quoted in the review are not in that file, the zones are proven by the frames above). The 3.6.27 fresh-discovery log carries no WHO 4 traffic at all (the plant fixture has no climate), so it is cited as neutral. runtime_behaviour.md L15 / L37, climate.md, the b16 note and ("MH201", False, True) are updated; the tests that used an MH201 as the default sweep model drop *#4*0##, and the delegated-WHO 4 follower case moves to an MH200N so it still proves the forwarded path.
  2. Comments (3c2f5c9): *#4*Z## is expanded into the four dimension queries on the SCS gateways (F454 + #0 in issue_629 answers *#4*1##..*#4*15## in about 1.3 s; the F455 multiple timeouts #674 10 s hang was an F455 on actuator-only channels); the *#4*1*0## NACK on 11 of 12 images is the bus TX timeout of an unpopulated zone (live F461 zone 1 NACK after 2.07 s, rows 374-376) while the populated zone 2 answers in 119 ms (rows 521-525); _poll_answered stays off "until a command-session capture of *#4*Z*0## on an SCS-only gateway exists"; *#4*Z*0## kept; feat(climate): hydrate cold subordinate zones via one-shot *#4*Z## once live #0 central unit is confirmed #813 named for the #0 cold start. The body says the same.
  3. _polls_temperature (a4abfe6) is a pure property; the one-shot DEBUG line and the flag live in async_update, placed before the fan / humidity early return so a humidity zone behind an F422 on an F454 still logs once while sending only *60## (an independent second-opinion review caught that ordering). Tests: nothing logged by __init__ or by reading the property, one record on the first update, none on the second, F455 interface zone silent, the humidity case.
  4. Compatibility table (2af4e8b): split rather than trimmed, because calibrate_discovery_with_ownd recomputes column 2 from the OWNd profile and verify_profiles_against_oracle requires equality with it. Column 2 stays "Profile Subsystems (advertised)"; a new column "Startup Sweep Requests (sent)" comes from a validated per-row sweep_omits (only *#4*0## / *#16*0*5## of an advertised WHO are accepted), and test_hardware_compatibility_table_matches_the_sweep compares it with _discovery_frames() for every row. MH200 and MH200N are separate rows, the MH201 note (refusal plus *#4*<WHERE>*0##) and an H4684 / HL4684 row are added; regenerated with sync_documentation.py --update.
  5. services.md (da8c29b): default myhome.sweep_bus sends zero WHO 4 frames on F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1 and triggers no per-zone poll; force: true sends them and the firmware drops them.
  6. Tests (d3be161): tests/test_startup_requests_per_firmware.py loads f461_live_2026-10-23, myhomeserver1_live_2026-10-24 and the issue_466 MH201 sweep plus its sibling with the loaders the other trace modules use, and replays the captured frames (F461 zone 2 ack / zone 1 NACK / *#4*0## refused locally; an MH202 zone sends exactly the captured *#4*2*0##; MyHomeServer1 zones 35..70 on a MyHomeServer1 handler; the MH201 sweep frame 11 with no zone reply and its zone 4 polling *#4*4*0##); the synthetic zone 3 cases are kept and labelled. In the body, run6 is credited only with the bus-visible replies and the tx omission is attributed to the HA debug log of the live retest, quoting what HA logged. 70 cases (was 60); 15 of the new / changed ones fail on the previous head.

Full suite 4631 passed / 1 skipped, custom_components at 100 % statement coverage; ruff, typing ratchet, HA standards, docs sync and translations clean.

gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 25, 2026
…del before the table lookup

`manifest.json` matches the H4684 over SSDP as `modelName: HL4684`, a spelling OWNd
has no alias for (it knows l4684, n4684, nt4684, lgrh4684), and an SSDP identity is
never overwritten by WHO 13 / 1013. "H4684" is not a substring of "HL4684" or of
"BTICINOL4684", so an SSDP-discovered or brand-prefixed H4684 missed the humidity
table and was polled (review 5644219403, finding 3).

`_model_key` now resolves a brand-prefixed or catalogue string word by word through
OWNd's own aliases (`"BTicino L4684"` -> H4684, `"Legrand 003598"` -> F454,
`"Legrand 003565"` -> MH200N, kept distinct from MH200), maps the SSDP spelling
`HL4684` (and `HC4684`) to `H4684` the way OpenWebNet-HA#794 does, and only then falls back to
the longest-first substring lookup. OpenWebNet-HA#794's two `_model_key` tests are copied as
they are (`test_aliased_model_strings_follow_their_profile`,
`test_ssdp_model_string_hl4684_is_keyed_as_h4684`); eleven brand-prefixed and
catalogue strings and two humidity zones (`HL4684`, `BTicino L4684`) are added.
12 fail before, 85 pass after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 25, 2026
…del before the table lookup

`manifest.json` matches the H4684 over SSDP as `modelName: HL4684`, a spelling OWNd
has no alias for (it knows l4684, n4684, nt4684, lgrh4684), and an SSDP identity is
never overwritten by WHO 13 / 1013. "H4684" is not a substring of "HL4684" or of
"BTICINOL4684", so an SSDP-discovered or brand-prefixed H4684 missed the humidity
table and was polled (review 5644219403, finding 3).

`_model_key` now resolves a brand-prefixed or catalogue string word by word through
OWNd's own aliases (`"BTicino L4684"` -> H4684, `"Legrand 003598"` -> F454,
`"Legrand 003565"` -> MH200N, kept distinct from MH200), maps the SSDP spelling
`HL4684` (and `HC4684`) to `H4684` the way OpenWebNet-HA#794 does, and only then falls back to
the longest-first substring lookup. OpenWebNet-HA#794's two `_model_key` tests are copied as
they are (`test_aliased_model_strings_follow_their_profile`,
`test_ssdp_model_string_hl4684_is_keyed_as_h4684`); eleven brand-prefixed and
catalogue strings and two humidity zones (`HL4684`, `BTicino L4684`) are added.
12 fail before, 85 pass after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck please rebase

@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from d3be161 to b1c4716 Compare October 25, 2026 19:47
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Rebased onto v2-phase1-architecture 5641f91 (after #792 and #815): the twelve commits replay with two resolutions in gateway.py (the #792 request tables and this PR's refusal tables now coexist, with _ZONE_REQUEST_MODELS keyed on all of them, and the SSDP HL4684 mapping folded into the base's _model_key) and one in climate.py (actuators and current_temperature_restored attributes side by side). Head b1c4716, mergeable; full suite 4752 passed / 1 skipped at 100 % coverage, ruff, typing ratchet, HA standards, docs sync and translations clean.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck please rebase

gdluck and others added 13 commits October 25, 2026 22:00
…ch zone's temperature on SCS-only gateways

own-firmware-oracle (results/mcp_index.json at 53f9514) shows *michnovka#4*0## NACKed
with nothing on the bus by F450, F454, F459, F460, F461, H4684, MH200, MH202
and MyHomeServer1 (trace-corpus suite) and *OpenWebNet-HA#16*0*5## by F450, F459, F460,
F461, MH202 and MyHomeServer1 (sound-who16-source suite). The startup sweep
sent both everywhere, and on SCS-only gateways a standalone thermostat stayed
unknown after a restart until it next reported by itself (OpenWebNet-HA#185 MH201, OpenWebNet-HA#257
F461, OpenWebNet-HA#674).

The sweep leaves those two requests out on the models that drop them; the
OWNd profile is read first (supports_general_heating_status, supports_who(16))
with the MyHOME table as the fallback. On the SCS-only models among them and
on the MH201 each climate zone asks for its own temperature with *michnovka#4*Z*0##
after the sweep, which every emulated gateway forwards; a bus-discovered zone
keeps that poll unless the revealing frame carried its temperature. The last
current_temperature is restored with the attribute
current_temperature_restored until the zone reports again. The MH200 sweep
expectation drops *michnovka#4*0## accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e status poll (review follow-up)

Review 5642254816, point 1. The F450 (020010) was in GENERAL_ZONE_REQUEST_REFUSED
but not in the supports_zone_status exclusion tuple, so its zones kept sending
*michnovka#4*Z##, which the firmware forwards to the bus like the F454 (trace-corpus.tsv:
the same four raw SCS dimension telegrams and a NACK, the OpenWebNet-HA#674 timeout), and
zone_temperature_poll_needed stayed False. "F450" joins the tuple, so each of
its zones asks *michnovka#4*Z*0## instead.

The F460 (020012) expands *michnovka#4*Z## like the F461 ($00499013000 in trace-corpus.tsv),
so supports_zone_status stays True and it leaves ZONE_TEMPERATURE_POLL, which
is now an explicit table of the SCS-only models that refuse *michnovka#4*0## plus the
MH201 (OpenWebNet-HA#185); F460, F461 and MyHomeServer1 are documented as answering the
zone status poll themselves. climate.md and runtime_behaviour.md say which
models poll *michnovka#4*<WHERE>## and which ask *michnovka#4*<WHERE>*0##.

The zone temperature poll table gains F450 (True) and F460 (False).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ops Z#4#II requests (review follow-up)

Review 5642254816, point 2. own-firmware-oracle PR OpenWebNet-HA#60 (thermo-zone-requests
suite, not published yet) shows *michnovka#4*2#4#01*0## nack/silent on F450, F454,
F459, F460, F461, F453, F453AV, MH200N, MH202 and MyHomeServer1 and forwarded
only on H4684 and MH200; the MH202 crashes its thermoregulation daemon on the
dimensionless *michnovka#4*2#4#01##. Of the SCS-only models that poll *michnovka#4*Z*0##, the
F450, F454, F459 and MH202 therefore cannot poll a zone behind an interface.

A new handler property interface_zone_request_refused reads the OWNd profile
first (supports_interface_zone_requests, which belongs in OWNd's profiles) and
falls back to INTERFACE_ZONE_REQUEST_REFUSED, the oracle row. _polls_temperature
is False for an interface-routed zone on such a gateway, logged once at DEBUG,
so the zone keeps its restored reading until the bus reports it; the H4684,
MH200 and MH201 (not in the oracle) poll *michnovka#4*Z#4#II*0##.

The deliberate omission of the _poll_answered callback on the dimension 0
request is now commented: *michnovka#4*Z*0## ends with a NACK on the command session on
11 of the 12 oracle gateways even when forwarded (thermo-ts10.tsv), which
cancels the written future and would count a healthy zone as unresponsive.

Tests: four models whose interface zones send nothing (and log once), three
that poll *michnovka#4*3#4#01*0##, the profile flag over the table both ways.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… drops unless forced (review follow-up)

Review 5642254816, point 3. myhome.sweep_bus built its own general request
list gated on the profile's WHOs only, so with force=False it still sent
*michnovka#4*0## and *OpenWebNet-HA#16*0*5## to the gateways whose firmware drops them, each costing
a NACK and a BUS_QUIET_PERIOD wait in send_paced. The service now applies the
same firmware gate as the startup sweep (handler._refused_by_gateway, read
through a helper that tolerates handler doubles); force=True sends everything,
as the services.yaml and services.md descriptions now say.

Tests: an F461 sweep sends *#1*0## and *#2*0## with force=False and all five
general requests with force=True; the multi-gateway service test expects the
MH202 secondary to leave out *michnovka#4*0## and *OpenWebNet-HA#16*0*5## (43 status requests
instead of 45).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n the H4684 tables (review follow-up)

Review 5642254816, point 4. manifest.json registers the H4684 under the SSDP
modelName "HL4684", which OWNd master does not alias to its H4684 profile
(it knows h4684, l4684, n4684, nt4684, lgrh4684, lgr4684), so such a gateway
gets the Generic profile with model_name "HL4684": "H4684" in norm was False
in supports_zone_status and _model_key missed GENERAL_ZONE_REQUEST_REFUSED and
ZONE_TEMPERATURE_POLL. _model_key now maps HL4684 to H4684 and the
supports_zone_status exclusion tuple lists HL4684, until OWNd aliases it.

The same substring check missed the spellings OWNd does alias (L4684, N4684,
NT4684, LGR4684 and the MyHOME_Suite item numbers such as 003598 for the
F454): an L4684 was keyed on the H4684 tables (its profile is the H4684's) but
kept supports_zone_status True, so it skipped *michnovka#4*0## and still polled *michnovka#4*Z##
instead of *michnovka#4*Z*0##. supports_zone_status now also judges the model OWNd
resolves the configured string to (get_gateway_profile(model).model_name).

Tests: HL4684 in the sweep and zone temperature poll tables, a pinned test of
the key, the zone status and the three refusal properties, and L4684, NT4684,
003598 and 03535 following their profile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ew follow-up)

Review 5642254816, point 5. The Day 0 blank-start matrix in
runtime_behaviour.md said WHO 4 and WHO 16 are discovered with *michnovka#4*0## and
*OpenWebNet-HA#16*0*5## unconditionally; both rows now name the models whose firmware drops
the request and say that a zone or amplifier there is discovered from its
first bus frame. GATEWAY_DISCOVERY_METADATA (scripts/update_gateway_profiles.py)
no longer claims "Sound (*OpenWebNet-HA#16*0*5##)" for the MH202 (77 of 77 WHO 16 frames
silent in sound-who16-source.tsv) and notes the dropped requests on the F454,
F461, MyHomeServer1 and MH200 rows; hardware-compatibility.md is regenerated
with scripts/sync_documentation.py --update.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…weep leaves it out (review follow-up)

A physical MH201 3.3.6 (WHO 13 code 200) sent *michnovka#4*0## during a startup sweep and no
zone frame followed, although zones 2, 3 and 4 report on that plant: compare
tests/fixtures/traces/issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json frame 11
with myhome_trace_MH201_all_2026-09-25T19-05-42.json frames 0-10. Until now the MH201 was
only in ZONE_TEMPERATURE_POLL, so every start sent the refused general request, waited the
quiet period and then polled each zone anyway. "MH201" joins GENERAL_ZONE_REQUEST_REFUSED;
runtime_behaviour.md, climate.md and the b16 release note list it; the tests that used an
MH201 as the default sweep model drop *michnovka#4*0## from their expectations (the delegated-WHO 4
follower test moves to an MH200N, whose firmware forwards the request).

Refs OpenWebNet-HA#185.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…planations in the poll comments (review follow-up)

The comments around the zone poll said that SCS gateways forward *michnovka#4*Z## to thermostats
that have no handler for it, with a 10 s timeout per zone (OpenWebNet-HA#674), and that *michnovka#4*Z*0## ends
with a NACK on live zones. Neither matches the evidence: the emulated SCS gateways expand
*michnovka#4*Z## into four bus queries ($0499ZZ3000 / 12 / 0E / 0D in trace-corpus.tsv) and an F454
with a 99-zone central unit answers *michnovka#4*1##..*michnovka#4*15## in ~1.3 s each (issue_629 log, lines
489-700); the 10 s hang of OpenWebNet-HA#674 was an F455 on actuator-only channels. The *michnovka#4*1*0## NACK on
11 of 12 oracle images is the bus TX timeout of an image with no thermostat wired, the same
NACK the live F461 gives for its unpopulated zone 1 after 2.07 s (f461_live_2026-10-23 rows
374-376), while its populated zone 2 answers *michnovka#4*2*0## with *michnovka#4*2*0*0239## and an ACK in
119 ms (rows 521-525). The per-zone dimension 0 request stays (one bus query, ~120 ms, kind
to single-worker gateways); the _poll_answered omission now says it waits for a
command-session capture of *michnovka#4*Z*0## on an SCS-only gateway. Cold-start hydration of the
subordinate zones through the central unit is OpenWebNet-HA#813.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…withheld F422 poll in async_update (review follow-up)

The property mutated _interface_poll_skip_logged and wrote the one-shot DEBUG line while
__init__ computed _poll_on_add, before the entity had an entity_id or was attached to hass,
and never when async_update actually skipped the poll. It is now a pure boolean built from
two pure helpers (_temperature_poll_wanted, _interface_poll_refused); async_update logs
once, at DEBUG, when it withholds the poll of a zone behind an F422. Tests: nothing is
logged by construction or by reading the property, one line on the first update and none
on the second; an F455 zone behind an interface logs nothing (no poll is wanted there).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s and the requests the sweep sends (review follow-up)

"Initial Discovery Subsystems Queried" listed WHO 4 and 16 for models whose note said the
sweep leaves them out. The column could not simply drop them: calibrate_discovery_with_ownd
recomputes queried_whos from the OWNd profile and verify_profiles_against_oracle requires
the two to match, so the column is split instead. Column 2 stays the profile's WHOs (still
validated against the oracle), a new column 3 is what the sweep sends, derived from a
per-row sweep_omits list naming the general requests the firmware drops (*michnovka#4*0## /
*OpenWebNet-HA#16*0*5##; the validator rejects any other frame or a WHO the profile does not advertise),
and a test compares sweep_omits with MyHOMEGatewayHandler._discovery_frames() for every row.
MH200 and MH200N get separate rows (the MH200 drops *michnovka#4*0##, the MH200N forwards it), the
MH201 row names its *michnovka#4*0## refusal (issue_466 physical sweep trace) and its per-zone
*michnovka#4*<WHERE>*0## poll, and the H4684 / HL4684 gets a row. hardware-compatibility.md is
regenerated with sync_documentation.py --update.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the gateways that drop *michnovka#4*0## (review follow-up)

myhome.sweep_bus with force: false applies the startup sweep's firmware gate, so on F450,
F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1 it sends zero WHO 4
frames (no *michnovka#4*0##, and the service triggers no per-zone *michnovka#4*<WHERE>## or *michnovka#4*<WHERE>*0##
poll); *OpenWebNet-HA#16*0*5## is left out the same way on the six models that drop it. force: true
sends them and the firmware drops them before the bus.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…MH201 traces in the per-firmware tests (review follow-up)

The module synthesised zone 3 frames and loaded no trace. It now reads three committed
captures with the loaders the other trace tests use and pins what they show:
f461_live_2026-10-23 run 1 (*michnovka#4*2*0## answered with *michnovka#4*2*0*0239## and an ACK in 0.119 s,
*michnovka#4*1*0## of the unpopulated zone 1 NACKed after 2.071 s, *michnovka#4*0## refused in under 10 ms),
myhomeserver1_live_2026-10-24 (33 zones 35..70 reporting *michnovka#4*Z*0*TTTT##, *michnovka#4*0## refused in
51 ms, *michnovka#4*35## answered with five frames) and the issue_466 MH201 sweep (frame 11 *michnovka#4*0##
with no zone frame afterwards, against the sibling trace's zones 2, 3 and 4). The captured
replies are replayed through zone entities: the request an SCS-only zone 2 sends is the
captured *michnovka#4*2*0##, the captured reply clears the restored temperature, the MyHomeServer1
zones need no temperature poll and keep *michnovka#4*Z##, the MH201 leaves *michnovka#4*0## out and polls
the captured zone 4. A dedicated case shows a cancelled dimension 0 write never reaches
_poll_answered. The synthetic zone 3 cases stay (they pin the request, not a plant).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… temperature poll

After the rebase on the merged OpenWebNet-HA#793, a humidity-refusing SCS-only zone (F450,
F454, MH202) no longer sends nothing: it asks *michnovka#4*Z*0## for its temperature
(this PR) and still no *60##. The "never reaches _poll_health" case moves to
the models that forward the sweep's *michnovka#4*0## (F453, F453AV, MH200N), the F450
case asserts the temperature poll instead of a zone status it no longer has,
and the F422 humidity-zone case uses an F459 (forwards *60##, drops Z#4#II).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/gateway-startup-requests-per-firmware branch from b1c4716 to 13240f2 Compare October 25, 2026 20:07
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Rebased again onto v2-phase1-architecture 217b20e (after #793): the twelve commits replay on #793's merged resolver (its _model_key / KNOWN_GATEWAY_MODELS now also key this PR's three tables, _ZONE_REQUEST_MODELS unions all of them, and the SSDP HL4684 mapping is the one #793 brought), with one semantic reconciliation in the tests (13240f2): a humidity-refusing SCS-only zone (F450, F454, MH202) now sends its per-zone *#4*Z*0## temperature poll from this PR and still no *60##, so #793's "nothing is sent" cases move to the models that forward the sweep (F453, F453AV, MH200N) and the F422 humidity-zone case uses an F459. Head 13240f2, mergeable; 4855 passed / 1 skipped at 100 % coverage, ruff, typing ratchet, HA standards, docs sync and translations clean.

This branch has not been deployed

No deployments
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