Repository navigation
fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways - #794
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… 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>
911c77d to
d4284e4
Compare
d4284e4 to
5dbb70a
Compare
|
@gdluck please rebase and solve conflcts |
5dbb70a to
f1b1d9e
Compare
|
Rebased on the current base (head f1b1d9e, mergeable): |
f1b1d9e to
9b9b12a
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
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):
F450/F460mismatch (gateway.py,climate.md,test_startup_requests_per_firmware.py): Neither"F450"nor"F460"is insupports_zone_status's exclusion list, sozone_temperature_poll_neededreturnsFalsefor both — causing legacy SCS gatewayF450(020010) to send*#4*<WHERE>##(#674 timeout hazard) instead of*#4*<WHERE>*0##, whileclimate.mdmisclassifiesF461-siblingF460(020012) as SCS-only.- F422 interface-routed zones (
climate.py):*#4*2#4#01*0##isnack, silent(dropped locally by firmware) onF450,F454,F459, andMH202inOpenWebNet-HA/own-firmware-oracle#60. myhome.sweep_bus(custom_components/myhome/services.py:241-270): Whenforce=False,sweep_busbuilds its owngeneral_querieslist and gates only on_handler_supports_who(handler, who)without checkinghandler._refused_by_gateway(q). As a result,myhome.sweep_bus(force=False) still sends refused*#4*0##and*#16*0*5##frames and waitsBUS_QUIET_PERIODper NACKed frame insend_paced(). Please filtergeneralbynot handler._refused_by_gateway(q)whennot force(or reusehandler._discovery_frames()) and add a test forforce=Falsevsforce=True.- 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 onown-firmware-oracle#60. Note that*#4*1*0##is already published across all 12 emulated gateways inresults/<P>/<V>/oracle/full/thermo-ts10.tsv(and already intests/golden/firmware_oracle.json, checked byscripts/update_gateway_profiles.py:502):ack, out(93 01 14 00) onMH202andnack, out(93 01 14 00/$004990001000\r/ internal bus payload) on the other 11 models. You can citethermo-ts10.tsvdirectly alongside#60(which adds zone 2,#0/#23#1, and#4#01coverage), so this PR doesn't need to wait on#60once the items here are addressed.
| 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 |
There was a problem hiding this comment.
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 intrace-corpus.tsvis identical toF454/F459/H4684/MH200(4 raw SCS dimension telegrams93 01 14 00/0C/0E/0D+NACK, hitting the F455 multiple timeouts #674 10 s command-session timeout per standalone thermostat). Skipping*#4*0##onF450while leaving"F450"out ofsupports_zone_statusmakesclimate.pysend*#4*<WHERE>##instead of*#4*<WHERE>*0##."F450"should be added tosupports_zone_status's exclusion tuple.F460(020012): Sibling ofF461(020012) that expands*#4*1##via$00499013000\rjust likeF461. Keepingsupports_zone_status == True(zone_temperature_poll_needed == False) matchesF461, soF460should be removed from the SCS-only list indocs/configuration/climate.md(and either dropped fromZONE_TEMPERATURE_POLLor noted as gated out bysupports_zone_status).
| 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()) |
There was a problem hiding this comment.
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.
| 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)) |
There was a problem hiding this comment.
Two protocol nuances on this branch:
- F422 interface-routed zones (
Z#4#II): Whenself._interface is not None,self._full_whereisf"{self._where}#4#{self._interface}"(e.g.2#4#01). PerOpenWebNet-HA/own-firmware-oracle#60(*#4*2#4#01*0##), that frame isnack, silent(dropped locally without reaching the bus) onF450,F454,F459, andMH202(forwarded only onH4684andMH200). Consider gating_polls_temperatureoff whenself._interface is not Noneon gateways whose firmware drops#4#IIWHO 4 dimension requests (or qualifying this comment andclimate.md). - Intentional omission of
_poll_answered: Unlike*#4*Z##above,*#4*Z*0##terminates withNACKon the command session across 11 of 12 oracle gateways even when forwarded to the SCS bus (thermo-ts10.tsv), which cancelswritteninCommandWorkerPool. Not attaching_poll_answeredhere is therefore the right call so healthy SCS zones are not falsely flagged unresponsive — worth a brief comment so nobody "fixes" it later.
| * **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. |
There was a problem hiding this comment.
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).
| | `*#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) | |
There was a problem hiding this comment.
Two small doc follow-ups nearby:
- In line 15,
F460polls*#4*<WHERE>##rather than*#4*<WHERE>*0##(supports_zone_status == True, same020012translator family asF461), so"their SCS-only zones ask *#4*<WHERE>*0## instead"is subtle whenF460,F461, andMyHomeServer1are 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 (andGATEWAY_DISCOVERY_METADATA["MH202"]["notes"]inscripts/update_gateway_profiles.py/hardware-compatibility.mdstill saysFull support including ... Sound (*#16*0*5##)). On gateways that drop*#4*0##/*#16*0*5##, blank-start discovery with no registry entries ormyhome.yamlhappens on first bus traffic rather than from the startup sweep.
| @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 |
There was a problem hiding this comment.
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).
9b9b12a to
5985b69
Compare
|
Thanks for the thorough pass; every item is on the branch (head 5985b69, rebased on the current base, one commit per point):
Full suite 4584 passed / 1 skipped at 100 % coverage; ruff, typing ratchet, HA standards, docs sync and translations clean; |
|
All checks green on 5985b69; marking ready for review. |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
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):
MH201double-queries*#4*0##+*#4*Z*0##and overlooks two committed physicalMH201traces (gateway.py:158-171,runtime_behaviour.md:15/37,2.0.0b16.md).- 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'sF45510 s socket timeout and the busless QEMU oracle'sNACKwith liveF454 + #0(issue_629) and populated*#4*Z*0##(f461_live_2026-10-23). Cold-start#0subordinate-zone setpoint hydration is tracked separately in #813 so this PR stays focused. - Make
_polls_temperaturea pure property (climate.py:397, 412-433) — right now it mutates_interface_poll_skip_loggedand logsDEBUGduring__init__before the entity is attached tohassor the registry. - 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 lists4and16on models whose Column 3 note says the sweep omits them, and add the missingMH201/HL4684notes. - Document in
docs/configuration/services.md(L139-145) thatmyhome.sweep_bus(force: false) now sends zero WHO 4 frames on the gateways refusing*#4*0##(and does not trigger per-zone polls). - Back
tests/test_startup_requests_per_firmware.pywith real fixture frames (it currently loads zero trace files and uses synthetic Zone3frames despite the PR body stating trace frames are inlined there), and tighten the PR body'srun6_event_session_ha_fixed_integrationtable entry (run6is anevt-onlyrxcapture and cannot by itself provetxomissions).
(Separate trace-corpus schema, timestamp causality, issue_674 forensics, and fixture privacy-gate follow-ups are tracked in #814.)
| 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"}) |
There was a problem hiding this comment.
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:
tests/fixtures/traces/issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json:frame[11]senttx *#4*0##on a physicalMH201(fw 3.3.6, WHO 13 code200) and received zero WHO 4 frames, whereas sibling tracemyhome_trace_MH201_all_2026-09-25T19-05-42.jsonproves active WHO 4 zones2, 3, 4on that exact plant (*#4*2*0*0199##,*#4*3*0*0215##,*#4*4*0*0181##).tests/fixtures/traces/mh201-fresh-discovery-20260930.log+tests/fixtures/plants/mh201_physical_plant/(MH201fw 3.6.27, Linuxbt_termocousin ofF454/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.
| 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 |
There was a problem hiding this comment.
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.
| # 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. |
There was a problem hiding this comment.
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:
*#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,0Dintrace-corpus.tsv) and close the command session cleanly (ACKonMH202,~1.5-2.0 sNACKwhen unanswered on the other 8). OnF454+ 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 swith all 5 frames (dim 0, 12, mode, 14, 13). The10 sTCP socket hang in#674occurred onF455(not in the oracle) on actuator-only channels (*#4*Z*19*0*0##) where nothing answereddim 12/14/13, and standalone thermostats do not answer all four dimensions.*#4*Z*0##NACK rationale (L601-604):*#4*1*0##ended inNACKon 11/12 oracle images because the QEMU oracle runs without a physical thermostat wired to the bus (bus TX timeout ->*#*0##, identical to unpopulated Zone1inf461_live_2026-10-23row[374-376]NACKing after2.07 s). On a populated live zone (f461_live_2026-10-23row[521-523],*#4*2*0##), the gateway returns*#4*2*0*0239##+*#*1##ACK in119 ms. Omitting_poll_answereduntil 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` | |
There was a problem hiding this comment.
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.
| | **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. | |
There was a problem hiding this comment.
Column 2 (Initial Discovery Subsystems Queried) now contradicts Column 3 (Profile Support Notes), and MH201 / HL4684 are incomplete/missing.
- Column 2 is titled
Initial Discovery Subsystems Queried, yet still lists4and16for MH202, F461, and MyHomeServer1 (and4for F454 and MH200) while Column 3 states "so the sweep leaves both out". Please updatescripts/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; noteMH200vsMH200Nalso differ onWHO 4). - MH201 (
L64) omits both its*#4*0##refusal (issue_466physical sweep trace) and its per-zone*#4*<WHERE>*0##poll (contradictingruntime_behaviour.md:15andclimate.md:59). - HL4684 / H4684 is present in
Supported Gateways(L22) andGENERAL_ZONE_REQUEST_REFUSED/ZONE_TEMPERATURE_POLL, but missing fromGateway Startup Discovery Capabilities.
| 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 | ||
|
|
There was a problem hiding this comment.
Load and replay actual captured frames from the trace fixtures, and align the PR body's trace claims.
- Under
## Where are the traces, the PR body claims "The frames are also inlined intests/test_startup_requests_per_firmware.py." Currently this file loads zero fixture files, mockssend_status_request = AsyncMock(), and synthesizes Zone3frames (*#4*3*0*0215##,*#4*3*0*0241##), whereasf461_live_2026-10-23has Zone2(*#4*2*0*0239##) andmyhomeserver1_live_2026-10-24has Zones35..70. Please add fixture-backed assertions loading the captured rows/frames fromf461_live_2026-10-23,myhomeserver1_live_2026-10-24, andissue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json. - In the PR body's
Where are the tracestable, row 2 citesrun6_event_session_ha_fixed_integrationfor "the sweep of the patched integration without*#4*0##/*#16*0*5##". Becauserun6is anevt-only passiverxcapture (0txrows,0cmdrows), gateway-dropped requests (*#4*0##,*#5*0##,*#16*0*5##), NACKed*#4*2*11##, and unanswered*#18*51*...##produce zeroevtframes whether HA sent them or not — please creditrun6only with the bus-visible replies (*#1*0##,*#2*0##,*#4*2##,*#4*2*60##,*#4*#0*20##) and attributetx-skip verification to the HA debug log from the live retest.
5985b69 to
d3be161
Compare
|
Thanks for the second pass; all six are on the branch (head d3be161, rebased on 089fef5, one commit per item).
Full suite 4631 passed / 1 skipped, |
…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>
…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 please rebase |
d3be161 to
b1c4716
Compare
|
Rebased onto |
|
@gdluck please rebase |
…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>
b1c4716 to
13240f2
Compare
|
Rebased again onto |
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 committedissue_466sweep 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 staysunknownafter 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_busalike), lets each climate zone on those SCS-only gateways ask for its temperature with*#4*Z*0##, and restorescurrent_temperaturefrom the last state with acurrent_temperature_restoredflag 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_674forensics and fixture privacy-gate follow-ups in #814.The per-zone
*#4*Z*0##request rests on the publishedthermo-ts10.tsvrow (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
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_refusedreadsgetattr(profile, "supports_general_heating_status", None)first and falls back toGENERAL_ZONE_REQUEST_REFUSED(F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202, MyHomeServer1) when the flag isNoneor absent. The MH201 is not in the oracle: the entry is the physicalissue_466sweep 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_refusedreadsprofile.supports_who(16)first and falls back toSOUND_STATUS_REQUEST_REFUSED(F450, F459, F460, F461, MH202, MyHomeServer1). Plain OWNdmasterstill 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 whichprofile.supports_who(16)alone is enough.interface_zone_request_refusedreadsgetattr(profile, "supports_interface_zone_requests", None)first and falls back toINTERFACE_ZONE_REQUEST_REFUSED(F450, F453, F453AV, F454, F459, F460, F461, MH200N, MH202, MyHomeServer1: the ten images on which*#4*2#4#01*0##isnack, silentin 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._discovery_framesskips 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_busapplies the same gate throughhandler._refused_by_gateway;force: truesends everything, asservices.yamlandservices.mdsay (withforce: falsethe service sends no WHO 4 frame at all on the listed gateways and triggers no per-zone poll).zone_temperature_poll_needed: only on SCS-only gateways (supports_zone_statusFalse). The profile flag wins when it is stated:supports_general_heating_statusTruemeans the sweep answers and no per-zone poll is needed,Falsemeans it is; only an unverified flag falls back toZONE_TEMPERATURE_POLL, an explicit table (F450, F454, F459, H4684, MH200, MH201, MH202).F450is in thesupports_zone_statusexclusion 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), andF460is not in the table: it expands*#4*Z##into one request like the F461 ($00499013000intrace-corpus.tsv) and answers it, so it keeps the zone status poll.climate.py: on those gateways a zone polls on add withOWNHeatingCommand.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 whereinterface_zone_request_refusedis True (F450, F454, F459, MH202 among the polling models); the H4684, MH200 and MH201 poll*#4*Z#4#II*0##._polls_temperatureis a pure boolean property (built from_temperature_poll_wantedand_interface_poll_refused);async_updatewrites the one-shot DEBUG line when it withholds the poll, also when a fan or humidity request keeps the update going. No_poll_answeredcallback 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_temperatureis restored from the last state and flaggedcurrent_temperature_restored: trueuntil the first*#4*Z*0*T##; a malformed restored value is ignored.discovery.py: a bus-discovered zone on such a gateway keeps its poll on add unless the revealing frame carried its temperature._model_keymaps the SSDP model stringHL4684(manifest.json; not an OWNd alias yet) toH4684, andsupports_zone_statusjudges the configured string and the model OWNd resolves it to (get_gateway_profile(model).model_name), soHL4684,L4684,NT4684,003598(F454) and03535(MH202) are treated like the model they stand for instead of polling*#4*Z##.*#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'ssweep_busleaves out*#4*0##and*#16*0*5##(43 status requests instead of 45).docs/configuration/runtime_behaviour.md(startup table and Day 0 matrix),gateways.md,climate.md,services.md, andhardware-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-rowsweep_omitslist inscripts/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 fromprofile.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 }Hardware Verification
*#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 restartcurrent_temperatureshows the last value withcurrent_temperature_restored: true, cleared at the first*#4*2*0*…##.*#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.issue_466captures: 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 ofGENERAL_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.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.andSkipping 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 showedcurrent_temperature: 24.0from*#4*2*0*0240##and nocurrent_temperature_restoredflag (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 andsweep_busgating are covered bytest_f461_sweep_in_fullandtest_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, thehardware-compatibility.mdsweep column against_discovery_frames()for every row,myhome.sweep_buson an F461 withforceFalse (*#1*0##,*#2*0##) and True (all five general requests), the two profile-flag overrides, thezone_temperature_poll_neededtable (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 byasync_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.f461_live_2026-10-23run 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_466MH201 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 thesweep_omitsvalidator (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: thesweep_bushelper never hides a request on a handler without the gate or on a double returning a non-bool.test_gateway_profiles.pycases, and the four MH201 sweep expectations intest_multi_gateway.py,test_blank_start.pyandtest_multi_gateway_regressions.py); all pass with the change.test_issue_304_310_batch.py::test_discovery_asks_an_mh200_for_its_amplifiersis updated for the intended change;test_issue_674_f455_climate_trace.py(includingtest_gateway_supports_zone_status_property) passes unchanged.master, f04d8aa, onPYTHONPATH): 4631 passed, 1 skipped, 100.0 % statement coverage ofcustom_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 theissue_466fixtures already in the repository.tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, runrun1_oracle_replay_readonly*#4*0##,*#16*0*5##(all 18 WHO 16 forms),*#5*0##NACK in under 10 ms, nothing on the event sessionrun1_oracle_replay_readonly(plant_reads, rows 521-525)*#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)run6_event_session_ha_fixed_integration(an event-sessionrxcapture: it shows replies, not what HA did not send)*#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 runtests/fixtures/traces/myhomeserver1_live_2026-10-24/myhome_trace_MyHomeServer1_live_2026-10-24.json, runsrun1_identify,run2_reads_thermo_sound_alarm_lightsandrun3_reads_lights_addresses_humidity*#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 MH201tests/fixtures/traces/issue_466/myhome_sweep_MH201_all_2026-09-25T19-11-35.json(frame 11) againstmyhome_trace_MH201_all_2026-09-25T19-05-42.json(frames 0-10)*#4*0##sent, no zone frame follows; zones 2, 3 and 4 report on the same planttests/test_startup_requests_per_firmware.pyloads the first, fourth and fifth archives and replays their rows (see "How is it tested").What does the Firmware Oracle / Encyclopedia say
functional/who-4-temperature-control/addressing.md,dimensions.md):WHERE 0addresses the general probes ("all probes");DIMENSION 0"measured temperature" is read-only, four decimal digits0000..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.results/mcp_index.json53f9514):*#4*0##(results/<P>/<V>/oracle/full/trace-corpus.tsv): nack, silent on F450020010, F454020051, F459020105, F460020012, F461020011, H4684020054, MH200020102, MH202010024, MyHomeServer1028206; nack, out (forwarded) on F453010100, F453AV030014, MH200N010108. That row plus the MH201 capture isGENERAL_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$00499013000and 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.loglines 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 thesupports_zone_statusexclusion tuple and keeps F460 out ofZONE_TEMPERATURE_POLL.*#4*1*0##(results/<P>/<V>/oracle/full/thermo-ts10.tsv, also intests/golden/firmware_oracle.json, the WHO 4 probe ofscripts/update_gateway_profiles.py): ack, out on MH202010024; nack, out with the bus payload93 01 14 00/$004990001000on 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_answeredcallback 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 isSOUND_STATUS_REQUEST_REFUSED.#0/#23#1and#4#01coverage:*#4*2*0##out on every image (consistent with thethermo-ts10.tsvrow 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 isINTERFACE_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)
supports_zone_statusexclusion tuple, F460 leavesZONE_TEMPERATURE_POLL(now explicit) and the SCS-only list inclimate.md;runtime_behaviour.mdline 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 -> ede6721interface_zone_request_refused(profile flag first,INTERFACE_ZONE_REQUEST_REFUSEDfallback),_polls_temperatureoff for such zones on those models with one DEBUG line, the_poll_answeredomission commented,climate.mdqualified, tests -> 329ec0amyhome.sweep_buswithforce=Falseskipshandler._refused_by_gateway(q)frames,force=Truesends them; F461 test both ways, MH202 multi-gateway expectation 45 -> 43,services.yaml/services.md/ release notes -> 06afedfHL4684keyed asH4684in_model_keyand listed in thesupports_zone_statustuple; the OWNd-aliased spellings (L4684, NT4684, item numbers) follow their profile's model name insupports_zone_status(found while cross-checking the diff); tests -> 8dcd9bcGATEWAY_DISCOVERY_METADATAnotes for MH202 (no more "Sound (*#16*0*5##)"), F454, F461, MyHomeServer1, MH200,hardware-compatibility.mdregenerated -> a0ff59athermo-ts10.tsvrow*#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 andsweep_bus.(The shas are those after the rebase on the current
v2-phase1-architecture.)Review follow-up (review 5642844379)
"MH201"added toGENERAL_ZONE_REQUEST_REFUSEDon theissue_466evidence (sweep frame 11*#4*0##with no zone frame, sibling trace zones 2, 3, 4);runtime_behaviour.mdlines 15 / 37,climate.md, the b16 release note and("MH201", False, True)intest_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) -> e4d8adcclimate.pycomments corrected: SCS gateways expand*#4*Z##into the four dimension queries (F454 +#0answers 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_answeredstays 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#0track; this body corrected the same way -> 3c2f5c9_polls_temperatureis a pure boolean property; the one-shot DEBUG line and_interface_poll_skip_loggedmoved intoasync_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 -> a4abfe6hardware-compatibility.md: the column is split rather than trimmed, becausecalibrate_discovery_with_owndrecomputesqueried_whosfrom 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-rowsweep_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 withsync_documentation.py --update-> 2af4e8bservices.md:myhome.sweep_buswithforce: falsesends zero WHO 4 frames on F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1 and triggers no per-zone poll;force: truesends them and the firmware drops them -> da8c29btests/test_startup_requests_per_firmware.pyloadsf461_live_2026-10-23(zone 2*#4*2*0*0239##, zone 1 NACK),myhomeserver1_live_2026-10-24(zones 35..70) and theissue_466MH201 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 ofrun6credits it with the bus-visible replies only and thetxomissions are attributed to the HA debug log of the live retest -> d3be161 + this bodyChecklist
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).*#4*Z*0##fallback: the publishedthermo-ts10.tsvrow 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.)handle_event()and do not manually subscribe viaasync_dispatcher_connect.pytest,ruff, andscripts/verify_ha_standards.pypass.🤖 Generated with Claude Code