Repository navigation
fix(climate): do not poll humidity (dimension 60) where the emulated firmware did not forward it - #793
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>
6850fac to
2bc1fd4
Compare
2bc1fd4 to
d8b3db7
Compare
|
You are right, that was a bare |
…he WHO 4 target gateway (OpenWebNet-HA#792 review, 5) Four edge cases of the per-model tables and the zone watch: - _model_key compared the profile's model_name with "Generic" instead of its class: get_gateway_profile("BTicino F461") is a GenericGatewayProfile named "BTicino F461", so the key became "BTICINOF461" and missed both ALL_ZONE_REQUESTS and ACTUATOR_REQUEST_REFUSED. The key is now taken from a known profile's name, otherwise from the configured model, and a known model is looked up as a substring (longest first), like command_session_limit and supports_zone_status do. - The refusal and the all-zone form were read from this handler while send_status_request routes WHO 4 through the delegated secondary or the warm standby. _who4_target() resolves the same target (_delegated_target / _failover_target) and its tables decide: a primary F461 failed over to an MH202 sends nothing, failed over to an F454 asks per zone; an MH202 delegating WHO 4 to an F454 asks the F454. - A secondary probe (WHERE >= 100, push-only; *michnovka#4*105*...## NACKs in 2.1 s on the F461, docs/configuration/climate.md) no longer registers a zone watch. - MyHomeServer1 gets the all-zone form: in myhomeserver1_live_2026-10-24 (3.87.13, run2_reads_thermo_sound_alarm_lights) *michnovka#4*#0*20## was ACKed in 0.39 s with 43 actuator frames for 34 zones, where the per-zone requests take about 32 s through the paced sender; the emulated 028206 forwards it ("out"). Review 5639409551 on OpenWebNet-HA#792, finding 5. Draft OpenWebNet-HA#793 carries the same _model_key helper and needs the same change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@gdluck we will keep this in draft until the oracle outcome can be attached to this pr |
|
Agreed, it stays draft until OpenWebNet-HA/own-firmware-oracle#60 is in and the published rows can be cited here. The branch is meanwhile kept rebased and aligned with #792 (shared |
d8b3db7 to
7ab740c
Compare
…he WHO 4 target gateway (OpenWebNet-HA#792 review, 5) Four edge cases of the per-model tables and the zone watch: - _model_key compared the profile's model_name with "Generic" instead of its class: get_gateway_profile("BTicino F461") is a GenericGatewayProfile named "BTicino F461", so the key became "BTICINOF461" and missed both ALL_ZONE_REQUESTS and ACTUATOR_REQUEST_REFUSED. The key is now taken from a known profile's name, otherwise from the configured model, and a known model is looked up as a substring (longest first), like command_session_limit and supports_zone_status do. - The refusal and the all-zone form were read from this handler while send_status_request routes WHO 4 through the delegated secondary or the warm standby. _who4_target() resolves the same target (_delegated_target / _failover_target) and its tables decide: a primary F461 failed over to an MH202 sends nothing, failed over to an F454 asks per zone; an MH202 delegating WHO 4 to an F454 asks the F454. - A secondary probe (WHERE >= 100, push-only; *michnovka#4*105*...## NACKs in 2.1 s on the F461, docs/configuration/climate.md) no longer registers a zone watch. - MyHomeServer1 gets the all-zone form: in myhomeserver1_live_2026-10-24 (3.87.13, run2_reads_thermo_sound_alarm_lights) *michnovka#4*#0*20## was ACKed in 0.39 s with 43 actuator frames for 34 zones, where the per-zone requests take about 32 s through the paced sender; the emulated 028206 forwards it ("out"). Review 5639409551 on OpenWebNet-HA#792, finding 5. Draft OpenWebNet-HA#793 carries the same _model_key helper and needs the same change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7ab740c to
290e4bb
Compare
…he WHO 4 target gateway (OpenWebNet-HA#792 review, 5) Four edge cases of the per-model tables and the zone watch: - _model_key compared the profile's model_name with "Generic" instead of its class: get_gateway_profile("BTicino F461") is a GenericGatewayProfile named "BTicino F461", so the key became "BTICINOF461" and missed both ALL_ZONE_REQUESTS and ACTUATOR_REQUEST_REFUSED. The key is now taken from a known profile's name, otherwise from the configured model, and a known model is looked up as a substring (longest first), like command_session_limit and supports_zone_status do. - The refusal and the all-zone form were read from this handler while send_status_request routes WHO 4 through the delegated secondary or the warm standby. _who4_target() resolves the same target (_delegated_target / _failover_target) and its tables decide: a primary F461 failed over to an MH202 sends nothing, failed over to an F454 asks per zone; an MH202 delegating WHO 4 to an F454 asks the F454. - A secondary probe (WHERE >= 100, push-only; *michnovka#4*105*...## NACKs in 2.1 s on the F461, docs/configuration/climate.md) no longer registers a zone watch. - MyHomeServer1 gets the all-zone form: in myhomeserver1_live_2026-10-24 (3.87.13, run2_reads_thermo_sound_alarm_lights) *michnovka#4*#0*20## was ACKed in 0.39 s with 43 actuator frames for 34 zones, where the per-zone requests take about 32 s through the paced sender; the emulated 028206 forwards it ("out"). Review 5639409551 on OpenWebNet-HA#792, finding 5. Draft OpenWebNet-HA#793 carries the same _model_key helper and needs the same change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
OpenWebNet-HA/own-firmware-oracle#60 is merged (main 52d354b): the table's source is now |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Audit of PR #793 (290e4bb7)
Branch / Base: gdluck:fix/climate-humidity-poll-gate (290e4bb7) → v2-phase1-architecture (089fef51) — 5 files (+217 / -2)
Verification Run:
- GitHub CI (
290e4bb7): 17 / 17 check-runs green (OWNdpinned/latest/dev, pytest + coverage gate,mypy-ratchet,validate, HA Container & Compatibility stable/beta/dev,quality_scale.yaml). - Local WSL Suite (
Python 3.14.7, HA2026.10.1,OWNd==2.0.0b11& editableOWNd):ruff check,scripts/verify_ha_standards.py,scripts/sync_documentation.py --check, andscripts/typing_ratchet.py(mypy --strict: 0 error(s)) all pass;tests/test_climate_humidity_poll_gate.pypasses 36 / 36 against both pinnedOWNd==2.0.0b11andOWNdmasterwith 100% diff statement coverage. - Protocol & Trace Verification: Verified against the OpenWebNet Encyclopedia (
ownkb-build-0.9.0:WHO 4publicDIMENSIONtableownkb:chunk:r000573+ Living NowKW4691/F459/F460humidity timelineownkb:claim:c047609,c009734), OpenWebNet-HA/own-firmware-oracle#60 (52d354bb), and the committedf461_live_2026-10-23/myhomeserver1_live_2026-10-24captures. Unemulated gateways (F455,MH201,Generic) are conservatively excluded fromHUMIDITY_REQUEST_REFUSED, andsupports_heating_humidity_requestoverrides the fallback table whenisinstance(..., bool).
Changes Requested (Ordered by Severity)
1. [Medium — Bug] humidity_request_refused ignores shared-bus WHO 4 delegation and warm-standby failover
- Where:
custom_components/myhome/gateway.py:373-381,custom_components/myhome/climate.py:562, vsMyHOMEGatewayHandler.send_status_request(gateway.py:672-680). - What happens:
climate.pychecksself._gateway_handler.humidity_request_refusedon the primary gateway bound to the entity, whereassend_status_requestroutes WHO 4 frames through_delegated_target/_failover_target:- False suppression: Primary
MH202/F454delegatingWHO=4to secondaryF461/MyHomeServer1(or failing over to warm standbyF461) evaluateshumidity_request_refused == Trueonselfand skips*#4*Z*60##, even thoughsend_status_requestwould send throughF461/MyHomeServer1. - False poll: Primary
F461/MyHomeServer1failing over to warm standbyMH202/F454evaluateshumidity_request_refused == Falseonselfand sends*#4*Z*60##into the standbyMH202/F454, which NACKs it.
- False suppression: Primary
- Recommendation: Route
humidity_request_refusedthrough the same_who4_target()helper added in #792 (target = self._who4_target(); if target is not self: return target.humidity_request_refused) and add delegation + warm-standby failover test cases in both directions.
2. [Medium — Sibling PR Conflict & Divergent _model_key] _ZONE_REQUEST_MODELS differs from #792 and conflicts on merge
- Where:
custom_components/myhome/gateway.py:162, 357-370and.github/release-notes/2.0.0b16.md:34. - What happens:
- Commit
290e4bb7and the PR description state that#792and#793"no longer differ in what_model_keyreturns", which is inaccurate:#793builds_ZONE_REQUEST_MODELSfromHUMIDITY_REQUEST_REFUSEDalone (hasF450,F454; lacksF461,MYHOMESERVER1), whereas#792builds it fromACTUATOR_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS)(hasF461,MYHOMESERVER1; lacksF450,F454). - On
#793,_handler(hass, "BTicino F461")._model_keyreturns"BTICINOF461"rather than"F461", contradicting_model_key's own docstring ("""The gateway model the request tables are keyed on (F461, MYHOMESERVER1)."""). git merge-tree --write-treebetween#792(4a41da61) and#793(290e4bb7) conflicts in bothcustom_components/myhome/gateway.pyand.github/release-notes/2.0.0b16.md.
- Commit
- Recommendation: Key
_ZONE_REQUEST_MODELSoff the full set of known gateway models (including forwarding models such asF461,F459,F460,MYHOMESERVER1) so_model_keyactually returns identical values across both PRs, and coordinate/rebase once the first of#792/#793lands.
3. [Low — Edge Case] SSDP HL4684 and vendor-prefixed catalog aliases miss _model_key fallback
- Where:
custom_components/myhome/gateway.py:369-370,custom_components/myhome/manifest.json:25,custom_components/myhome/const.py:502-526. - What happens:
manifest.jsonmatches SSDP"modelName": "HL4684", whileOWNd.profiles.get_gateway_profile("HL4684")(and vendor-prefixed alias/catalog strings like"BTicino L4684","Legrand 003598"forF454,"Legrand 003565"forMH202,"Legrand 003535"forF453AV) returnsGenericGatewayProfile.- Because
"H4684"is not a contiguous substring of"HL4684"or"BTICINOL4684",_model_keyevaluates to"HL4684"/"BTICINOL4684"and missesHUMIDITY_REQUEST_REFUSED— and because SSDP is a technical identity,resolve_gateway_identity(identity.py:13-15) never overwritesmodelNamewhen WHO=13/1013 arrives.
- Recommendation: Normalize
HL4684/L4684/HC4684→H4684(and brand/catalog aliases via_SAME_PRODUCTinidentity.py, without collapsingMH200N→MH200) in_model_key, and add a test for"HL4684"/"BTicino L4684".
4. [Low — Code & PR Hygiene]
- Early-exit guard in
MyHOMEClimate.async_update(custom_components/myhome/climate.py:529-546vs:562):not self._humidityat line533still keepsasync_update()running onMH202/F454even whenhumidity_request_refusedisTrue(andnot self._central,not self._gateway_supports_zone_status,not self._fan), causingself._poll_health.should_skip(...)at line543to run and potentially raise/log on a zone that was never going to send a single status request. Factoringwants_humidity = self._humidity and getattr(self._gateway_handler, "humidity_request_refused", False) is not Trueinto the early return avoids entering_poll_healthwhen no poll will be sent (while keeping_poll_on_add = Truein__init__/_enable_humidityso startup identity resolution before_async_poll_after_discoveryis preserved).
- Oracle TSV publication & PR body cleanup:
- On
OpenWebNet-HA/own-firmware-oracle, thesuites.ymlrun for52d354bb(#40185238079) completed and pushed commitd3767f49(results/<P>/<V>/oracle/full/thermo-zone-requests.tsv+ regeneratedresults/mcp_index.json) to branchoracle/update-suites, which still needs a PR tomainbefore the published TSVs and MCP index are onmain. - Under
## Checklist, item 2 is still unchecked (- [ ]) and still says "that is why this PR is a draft". - Under
## Where are the traces, the PR states "The frames are also inlined intests/test_climate_humidity_poll_gate.py", whereas the test file only asserts outgoing mock strings (*#4*2*60##) and never replays*#4*2*60*39##,*#4*2*60*38##, or*#4*39*60*51##from the committed captures. Either replay the captured humidity frames throughMyHOMEClimateor adjust the wording inWhere are the traces.
- On
| supported = getattr(getattr(self.gateway, "profile", None), "supports_heating_humidity_request", None) | ||
| if isinstance(supported, bool): | ||
| return not supported | ||
| return self._model_key in HUMIDITY_REQUEST_REFUSED |
There was a problem hiding this comment.
humidity_request_refused inspects self directly, whereas send_status_request (gateway.py:672-680) dispatches WHO 4 requests through _delegated_target and _failover_target.
If a primary MH202 / F454 delegates WHO=4 to a secondary F461 / MyHomeServer1 (or fails over to warm standby F461), this returns True and suppresses humidity polling even though the request would be sent through F461. Conversely, if primary F461 fails over to standby MH202, this returns False and sends *#4*Z*60## into the refusing standby.
Please route this through the same _who4_target() helper as #792:
target = self._who4_target()
if target is not self:
return target.humidity_request_refusedand add test coverage for both delegation and warm-standby failover directions.
| # wins, ``None`` = unverified); this table is the fallback until the flag exists there. | ||
| HUMIDITY_REQUEST_REFUSED = frozenset({"F450", "F453", "F453AV", "F454", "H4684", "MH200", "MH200N", "MH202"}) | ||
| # The models the table knows, longest first so ``MH200N`` is not read as ``MH200``. | ||
| _ZONE_REQUEST_MODELS = sorted(HUMIDITY_REQUEST_REFUSED, key=len, reverse=True) |
There was a problem hiding this comment.
_ZONE_REQUEST_MODELS here only contains HUMIDITY_REQUEST_REFUSED, so _model_key does not return the same value as #792 (e.g. "BTicino F461" returns "BTICINOF461" here vs "F461" in #792, contradicting line 358's docstring, while #792 lacks F450 and F454). This line also merge-conflicts directly with #792.
Include the known forwarding models (F459, F460, F461, MYHOMESERVER1) or union all known gateway model keys so _model_key behaves identically in both PRs.
| else: | ||
| name = self.model | ||
| normalized = "".join(character for character in str(name).upper() if character.isalnum()) | ||
| return next((model for model in _ZONE_REQUEST_MODELS if model in normalized), normalized) |
There was a problem hiding this comment.
manifest.json (line 25) advertises SSDP "modelName": "HL4684", for which get_gateway_profile("HL4684") (as well as vendor-prefixed strings like "BTicino L4684") returns GenericGatewayProfile. Because "H4684" is not a substring of "HL4684" or "BTICINOL4684" — and SSDP technical identities are never overwritten by resolve_gateway_identity — an SSDP-discovered HL4684 / BTicino L4684 misses HUMIDITY_REQUEST_REFUSED.
Please normalize HL4684 / L4684 / HC4684 (and _SAME_PRODUCT brand variants, keeping MH200N distinct from MH200) before the table lookup.
| cast(OWNCommand, OWNHeatingCommand.parse(f"*#4*{self._full_where}*11##")) | ||
| ) | ||
| if self._humidity: | ||
| if getattr(self._gateway_handler, "humidity_request_refused", False) is True: |
There was a problem hiding this comment.
Because the early-return guard at lines 529-534 checks not self._humidity unconditionally, a humidity-enabled zone on MH202 / F454 (_gateway_supports_zone_status == False, _fan == False) still runs self._poll_health.should_skip(...) at line 543 even when humidity_request_refused is True and zero frames will be sent.
Computing wants_humidity = self._humidity and getattr(self._gateway_handler, "humidity_request_refused", False) is not True and using not wants_humidity in the early-return guard avoids entering _poll_health when async_update() has nothing to send.
| assert isinstance(handler.gateway.profile, GenericGatewayProfile) | ||
| # HUMIDITY_REQUEST_REFUSED names no forwarding model, so the configured name is kept (#792's | ||
| # all-zone table adds F461 to the lookup); either way the key is not a refusing model. | ||
| assert "F461" in handler._model_key |
There was a problem hiding this comment.
Here handler._model_key is actually "BTICINOF461" (not "F461"), which is why assert "F461" in handler._model_key had to be weakened from == "F461" — directly contradicting commit 290e4bb7's claim that #792 and #793 return the same _model_key. Once _ZONE_REQUEST_MODELS includes the forwarding models, this can assert handler._model_key == "F461" cleanly; please also add cases for WHO 4 delegation/failover and "HL4684" / "BTicino L4684".
|
Thanks for the audit; all items are on the branch (head 51aee89, one commit per point).
4656 passed / 1 skipped at 100 % coverage; ruff (0.16.7 and 0.17.2), typing ratchet, HA standards, docs sync and translations clean; |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Audit of PR #793 (51aee891)
Branch / Base: gdluck:fix/climate-humidity-poll-gate (51aee891) → v2-phase1-architecture (2ece4744, post-#792)
Verification Run:
- Review
5644219403Follow-Up Verified (946abbd7..51aee891):humidity_request_refusedroutes through_who4_target(), covering shared-bus WHO 4 delegation and warm-standby failover in both directions (946abbd7).KNOWN_GATEWAY_MODELS,_MODEL_SPELLINGS, and_resolved_modelresolve SSDPHL4684/HC4684, brand-prefixed names ("BTicino F461"→"F461","BTicino L4684"→"H4684"), and catalog numbers (003598→F454,003565→MH200N,003535→MH202) without collapsingMH200NintoMH200(bce1d0d0,1d1be7d5).wants_humidityinMyHOMEClimate.async_update's early-return guard avoids entering_poll_health.should_skip(...)on refusing SCS gateways when nothing will be sent, while keepingF450's*#4*Z##poll counted and_poll_on_addintact (a70026ca).
- Local WSL Suite (
Python 3.14.7, HA2026.10.1, pinnedOWNd==2.0.0b11):ruff check,scripts/verify_ha_standards.py(54/54 rules),scripts/sync_documentation.py --check,scripts/manage_translations.py check,scripts/typing_ratchet.py(mypy --strict: 0 error(s)), andpytest tests/test_climate_humidity_poll_gate.py tests/test_trace_oracle_validation.py(168 passed) all pass with 100.0% diff statement coverage onclimate.pyandgateway.py.
Changes Requested Before Merge (Ordered by Severity)
1. [Blocker — Merge Conflict] Rebase onto v2-phase1-architecture (2ece4744, post-#792)
- Where:
custom_components/myhome/gateway.pyand.github/release-notes/2.0.0b16.md:34. - What happens: Now that #792 (
2ece4744) is merged,#793isDIRTY(mergeable: false) in both files:gateway.py:ACTUATOR_REQUEST_REFUSED,ALL_ZONE_REQUESTS,_ZONE_REQUEST_MODELS,_model_key, and_who4_target()..github/release-notes/2.0.0b16.md: line34(- **Thermoregulation (WHO 4)**:).
- Recommendation: Rebase onto
v2-phase1-architecture, keep a single_model_key(retaining#793'sKNOWN_GATEWAY_MODELS/_MODEL_SPELLINGS/_resolved_modelsuperset soactuator_request_refusedandall_zone_requestbenefit from the same alias normalization) and a single_who4_target(), union_ZONE_REQUEST_MODELS = sorted(KNOWN_GATEWAY_MODELS | ACTUATOR_REQUEST_REFUSED | HUMIDITY_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS), key=len, reverse=True), and merge both clauses on the WHO 4 release-notes bullet.
2. [Medium — Real-Trace Fidelity] Eliminate cross-plant F461 → MH202 frame mixing, qualify unverified upward-push claims, and replay through climate.async_setup_entry
- Where:
tests/test_climate_humidity_poll_gate.py:456-512,custom_components/myhome/climate.py:529-531,docs/configuration/climate.md:67. - What happens:
- Cross-plant trace mixing & unverified upward translation:
test_captured_pushes_keep_a_refusing_gateway_s_zone_fresh_without_a_poll(lines 470-488) feeds spontaneous*#4*2*60*38##pushes captured on a physicalF461(f461_live_2026-10-23run6) into a mockMH202handler, whileclimate.py:531anddocs/configuration/climate.md:67state unconditionally that onF450, F453, F453AV, F454, H4684, MH200, MH200N and MH202"the zone's own pushes keep it fresh there" / "their zones are not queried and show the humidity their probe pushes". Neither any physical capture intests/fixtures/traces/nor the firmware oracle verifies that those 8 older gateways (MH200from 2006,H4684,F453,F454,MH202, etc.) translate SCS humidity telegrams upward into*#4*Z*60*HH##on their event session at all (per Encyclopediaownkb:claim:c047609/c047969, Living NowKW4691added humidity in 2020 forF459and 2026 forF460/F461; the only other physical gateway traced emitting*#4*42*60*52##isF455intests/fixtures/traces/issue_674/myhome_trace_f455_climate_timeouts_2026-10-09.json, which is outsideHUMIDITY_REQUEST_REFUSED). - Direct
MyHOMEClimate(..., humidity=True)construction instead of dispatcher replay: Unlike#792(4a41da61), which replays plant captures end-to-end throughclimate.async_setup_entry+async_dispatcher_send(hass, f"myhome_message_{mac}", ...),lines 456-512pre-constructMyHOMEClimatewithhumidity=Trueand callzone.handle_event(...)directly, bypassing dynamic discovery and_enable_humidity(), and do not covermyhomeserver1_live_2026-10-24run2's captured*#4*35*60##NACK on unprobed zone 35 alongsiderun3's*#4*39*60*51##reply (orissue_674's authenticF455*#4*42*60*52##push).
- Cross-plant trace mixing & unverified upward translation:
- Recommendation:
- Replay
f461_live_2026-10-23(run6+run2),myhomeserver1_live_2026-10-24(run2zone 35 NACK /*#4*0*60##burst +run3zone 39 reply), andissue_674(F455*#4*42*60*52##spontaneous push) on their matching gateway models throughclimate.async_setup_entry+myhome_message_<mac>. - Keep any test of an
MH202entity receivingOWNHeatingEvent("*#4*2*60*38##")clearly framed as an integration-side unit test (e.g., shared-bus event delivery / YAMLhumidity: true) rather than claiming anF461capture representsMH202bus traffic, and rewordclimate.py:531anddocs/configuration/climate.md:67so they state that any*#4*<WHERE>*60*<HH>##push delivered on the event session still updates humidity, without asserting that those 8 gateways translate humidity pushes upward.
- Replay
3. [Low — Oracle TSV Publication & _model_key Tokenization]
- Publish
thermo-zone-requests.tsvtoown-firmware-oraclemain: Commitd3767f49onoracle/update-suitescontains the CI-generatedresults/<P>/<V>/oracle/full/thermo-zone-requests.tsvrows and regeneratedresults/mcp_index.json; merging that suites update toown-firmware-oraclemainensures the machine-readable TSVs and MCP oracle index backHUMIDITY_REQUEST_REFUSEDonmainalongsidefindings/thermo-zone-requests.md. - Tokenize
_model_keyon alphanumeric words (custom_components/myhome/gateway.py:405): Usingre.findall(r"[A-Za-z0-9]+", str(name))instead ofstr(name).split()ensures hyphenated or parenthesized model strings (e.g."BTicino-L4684","F454 (003598)") also resolve via_resolved_modelto"H4684"/"F454".
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Inline diff comments for the changes requested in #pullrequestreview-5644470090 (rebase onto v2-phase1-architecture after #792, strict real-trace fidelity without cross-gateway frame mixing, and own-firmware-oracle main TSV publication):
| # F460, F461 and MyHomeServer1 forwarded it, and the live F461 / MyHomeServer1 answer it. | ||
| # Read from the OWNd profile first (``supports_heating_humidity_request``: ``True`` / ``False`` | ||
| # wins, ``None`` = unverified); this table is the fallback until the flag exists there. | ||
| HUMIDITY_REQUEST_REFUSED = frozenset({"F450", "F453", "F453AV", "F454", "H4684", "MH200", "MH200N", "MH202"}) |
There was a problem hiding this comment.
- Merge conflict with
#792(2ece4744): Rebase ontov2-phase1-architectureand union_ZONE_REQUEST_MODELSacrossKNOWN_GATEWAY_MODELS | ACTUATOR_REQUEST_REFUSED | HUMIDITY_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS)while keeping one shared_model_keyand_who4_target(). - Oracle TSV publication: There are zero physical traces on these 8 models, and the machine-readable
thermo-zone-requests.tsvrows +results/mcp_index.json(OpenWebNet-HA/own-firmware-oracle@d3767f49) are still onoracle/update-suitesrather thanmain. Please get the suites update merged toown-firmware-oraclemainsoHUMIDITY_REQUEST_REFUSEDis backed by published oracle TSVs rather than prose-only findings.
| name = getattr(profile, "model_name", None) or self.model | ||
| else: | ||
| name = self.model | ||
| for word in str(name).split(): |
There was a problem hiding this comment.
Use re.findall(r"[A-Za-z0-9]+", str(name)) instead of str(name).split() so hyphenated or parenthesized model strings (e.g. "BTicino-L4684", "F454 (003598)") also resolve through _resolved_model to "H4684" / "F454".
| return | ||
| # Humidity is asked only where the gateway forwards the request: the emulated firmware | ||
| # of F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 did not (own-firmware-oracle | ||
| # thermo-zone-requests finding, emulated firmware); the zone's own pushes keep it fresh there. |
There was a problem hiding this comment.
Please drop the unverified assertion that "the zone's own pushes keep it fresh there": neither any physical trace in tests/fixtures/traces/ nor the firmware oracle verifies that these 8 older gateways (MH200, H4684, F453, F454, MH202, etc.) translate SCS humidity frames upward into *#4*Z*60*HH## on the event session at all. State only that any *#4*<WHERE>*60*<HH>## push received on the event session still updates current_humidity.
| * **Dynamic Auto-Detection**: When a climate zone emits a humidity reading on the SCS bus, the integration dynamically enables humidity tracking on that entity. | ||
| * **State Restoration Across Restarts**: The zone remembers that it supports humidity across Home Assistant restarts and restores its last known humidity value immediately. | ||
| * **Targeted Startup Query**: After startup (once the initial discovery sweep finishes), only climate zones that support humidity are queried with `*#4*<WHERE>*60##` to refresh the current reading. This avoids unnecessary bus traffic on zones without humidity sensors, and works across both software gateways (MyHomeServer1) and physical SCS gateways (such as F461, F454, F455, MH200N). | ||
| * **Targeted Startup Query**: After startup (once the initial discovery sweep finishes), only climate zones that support humidity are queried with `*#4*<WHERE>*60##` to refresh the current reading. This avoids unnecessary bus traffic on zones without humidity sensors. A live F461 and a live MyHomeServer1 answer the request (F459 and F460 forward it in the firmware emulation); on F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 the emulated firmware did not forward it (own-firmware-oracle `findings/thermo-zone-requests.md`, a `firmware_observed` verdict), so until the OWNd gateway profile states `supports_heating_humidity_request` their zones are not queried and show the humidity their probe pushes. |
There was a problem hiding this comment.
Same here: avoid stating that F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 "show the humidity their probe pushes" — upward SCS-to-OpenWebNet translation of dimension 60 has never been traced on those 8 models (only F461, MyHomeServer1, and F455 in #674 have physical dimension-60 traces). Qualify this to say that any *#4*<WHERE>*60*<HH>## event delivered by the gateway updates humidity without polling.
| - **Covers**: `class:` in `myhome.yaml` now reaches WHO 2 covers (a `curtain` is no longer shown as a shutter; #39); Venetian blind slat tilt as timed slat pulses (`slat_tilt`, `slat_time`) (#508); impulse garage and gate covers on WHO 1 relays with optional safety approval, door-entry strike locks and doorbell events (#667); native broadcast for general covers behind an interface (#679); a cover stop overtakes other queued frames (#694); cover kinematics split into pluggable strategies (#739). | ||
| - **Lighting**: 2-digit Area 0 point addresses `01`–`09` (#713); ON dimmers on a fresh install are probed for brightness support (#742); timed turn-on uses the OWNd builders (#659); lights the startup sweep already reported are not polled again (#701); area and group re-syncs only skip once every member has echoed, counting only the members' own echoes in the commanded state (#740, #749). | ||
| - **Thermoregulation (WHO 4)**: season frames, a generic setpoint mode and the central unit's season (#657, #725); holiday and vacation program attributes (#689); zone humidity remembered and queried at startup (#712); F461 zones polled, AUTO on standalone zones, fan coils per season, season kept across restarts (#714, #715); `hvac_action` derived from the season when the actuator carries no heating flag (#721); standalone default follows the plant (#698); 10 s command timeouts on the F455 removed (#675); migrated to the `native_*` temperature API (#665). | ||
| - **Thermoregulation (WHO 4)**: zone humidity is not polled on F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202, where the emulated firmware did not forward `*#4*<WHERE>*60##` (own-firmware-oracle `findings/thermo-zone-requests.md`, emulated firmware; an OWNd profile flag `supports_heating_humidity_request` overrides the table); season frames, a generic setpoint mode and the central unit's season (#657, #725); holiday and vacation program attributes (#689); zone humidity remembered and queried at startup (#712); F461 zones polled, AUTO on standalone zones, fan coils per season, season kept across restarts (#714, #715); `hvac_action` derived from the season when the actuator carries no heating flag (#721); standalone default follows the plant (#698); 10 s command timeouts on the F455 removed (#675); migrated to the `native_*` temperature API (#665). |
There was a problem hiding this comment.
Merge conflict with #792 (2ece4744) on the Thermoregulation (WHO 4) bullet — please rebase and combine both clauses cleanly.
| assert zone.current_humidity == 38.0 | ||
|
|
||
|
|
||
| async def test_captured_pushes_keep_a_refusing_gateway_s_zone_fresh_without_a_poll(hass, f461_rows): |
There was a problem hiding this comment.
Do not mix a live F461 capture (f461_live_2026-10-23 run6, *#4*2*60*38##) into a mock MH202 gateway under the captured-traces section as evidence that an MH202 plant pushes *#4*Z*60*HH## — we have zero physical traces of dimension 60 on MH202.
Please keep trace-backed tests strictly faithful to the gateway family each trace was captured on, replayed end-to-end through climate.async_setup_entry + async_dispatcher_send(hass, f"myhome_message_{mac}", ...) (as in #792 4a41da61) rather than pre-constructing MyHOMEClimate(..., humidity=True) directly:
F461(f461_live_2026-10-23run6spontaneous*#4*2*60*38##pushes enabling_humidity, andrun2*#4*2*60##→*#4*2*60*39##),MyHomeServer1(myhomeserver1_live_2026-10-24run2zone 35*#4*35*60##NACK +*#4*0*60##17-zone burst, andrun3zone 39*#4*39*60##→*#4*39*60*51##),F455(tests/fixtures/traces/issue_674/myhome_trace_f455_climate_timeouts_2026-10-09.jsonspontaneous*#4*42*60*52##push on an unlisted SCS gateway).
If you keep an integration unit test verifying that MH202 skips *#4*2*60## even after _enable_humidity(), keep it in the unit-test section above (e.g. shared-bus delivery / humidity: true config) rather than presenting an F461 trace as MH202 traffic.
…firmware did not forward it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bstring lookup (align with OpenWebNet-HA#792) _model_key compared the profile's model_name with "Generic" instead of its class: get_gateway_profile("Legrand MH200N") is a GenericGatewayProfile named "Legrand MH200N", so the key became "LEGRANDMH200N" and missed HUMIDITY_REQUEST_REFUSED, and the zone was polled on a gateway whose emulated firmware does not forward *michnovka#4*Z*60##. The key is now taken from a known profile's name, otherwise from the configured model, in which a known model is looked up as a substring (longest first, so MH200N is not read as MH200), like command_session_limit and supports_zone_status do. Same helper and the same change as OpenWebNet-HA#792 (review 5639409551, finding 5), so the two branches no longer differ in what _model_key returns. Fail-before / pass-after on tests/test_climate_humidity_poll_gate.py: 3 of the 7 new cases fail on the previous helper (Legrand MH200N, BTicino F453AV, the vendor-prefixed MH200N zone poll), 36 of 36 pass with the change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…umidity poll gate own-firmware-oracle PR OpenWebNet-HA#60 merged (main 52d354b, 2026-10-25): the table's source is now findings/thermo-zone-requests.md on main, a firmware_observed verdict on emulated firmware, no longer an unpublished run of an open PR. Comments, docs, release note and the test docstring say so; no behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arm-standby failover `send_status_request` routes a WHO 4 request through the secondary gateway WHO 4 is delegated to, or through the warm standby while the primary is disconnected, but `humidity_request_refused` judged the primary's own model: a primary MH202 / F454 delegating WHO 4 to an F461 / MyHomeServer1 skipped `*michnovka#4*Z*60##` although the request would have gone through the forwarding gateway, and a primary F461 failed over to a standby MH202 / F454 sent it into the refusing standby (review 5644219403, finding 1). The property now asks the same `_who4_target()` helper OpenWebNet-HA#792 adds (copied verbatim: `_delegated_target(probe) or _failover_target(probe) or self` on a fixed WHO 4 probe) and returns that gateway's verdict when it is not this one. Thirteen test cases: delegation to a forwarding / refusing secondary, failover to a refusing / forwarding standby (both directions, F461 / MyHomeServer1 against MH202 / F454), the secondary preferred to the standby, the standalone case unchanged, and two cases through the real topology lookup (a configured secondary with WHO 4 delegated and a configured standby, connected or not). 13 fail before, 49 pass after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… models included `_ZONE_REQUEST_MODELS` was built from `HUMIDITY_REQUEST_REFUSED` alone, so a forwarding model behind a brand prefix was not reduced to its model: `_model_key` of "BTicino F461" was "BTICINOF461", against the property's own docstring and against OpenWebNet-HA#792, whose lookup (`ACTUATOR_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS)`) knows F461 and MyHomeServer1 but not F450 and F454 (review 5644219403, finding 2). `KNOWN_GATEWAY_MODELS` now lists every model the tables can be keyed on: the twelve images the oracle emulates (refusing and forwarding) and the other models OWNd profiles (F452, F452V, F455, H4890, MH201), and `_ZONE_REQUEST_MODELS` is that set united with the refusing table, longest first as before. The test at the `BTicino F461` case asserts `_model_key == "F461"`, and every known model is checked behind a brand prefix (9 of the 17 fail on the previous lookup, the forwarding ones). 67 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>
…y poll is refused The early return of `async_update` tested `not self._humidity`, so a humidity zone on an MH202 / F454 (no zone status request, no fan) still ran `_poll_health.should_skip`, and could log and raise the unresponsive-zone repair, for a poll that was never going to be sent (review 5644219403, finding 4). `wants_humidity = self._humidity and humidity_request_refused is not True` is computed once and used both in the early return and in the send branch; the DEBUG line for a refused request stays. `_poll_on_add` is untouched (`_enable_humidity` still sets it, so the first update runs after the identity settled). Tests: the humidity zone of a refusing SCS gateway never reaches `_poll_health` (MH202, F454, Legrand MH200N), an F450 (refuses dimension 60, answers `*michnovka#4*Z##`) still sends and counts its zone poll, an F461 zone goes through `_poll_health` and is held back when it skips, and `_poll_on_add` after `_enable_humidity`. 3 fail before, 92 pass after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…HomeServer1 traces The PR body said the captured frames were inlined in the test module, while the module only asserted the outgoing request strings (review 5644219403, finding 4.2). The module now loads the two committed traces the way the other trace modules do and replays them through `MyHOMEClimate`: `f461_live_2026-10-23` run 2 (`*michnovka#4*2*60##` ACKed with `*michnovka#4*2*60*39##` in 0.14 s, echoed on the event session) and run 6 (three spontaneous `*michnovka#4*2*60*38##` pushes, fed to a zone on a refusing MH202 to show that the pushes enable and refresh humidity without a poll), and `myhomeserver1_live_2026-10-24` run 3 (`*michnovka#4*39*60##` ACKed with `*michnovka#4*39*60*51##`). Five cases, 97 pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n _model_key (OpenWebNet-HA#793 review, 2) _model_key resolved the configured model word by word on whitespace, so a hyphenated or parenthesised string ("BTicino-L4684") was one word OWNd does not know, and the substring fallback left it unresolved (L4684 is an alias, not a table key). The words are now re.findall(r"[A-Za-z0-9]+", name), so "BTicino-L4684" -> H4684 and "F454 (003598)" / "Legrand (003598)" -> F454 through _resolved_model, as the review asked. Fail-before / pass-after on tests/test_climate_humidity_poll_gate.py: of the five new strings "BTicino-L4684" fails on the whitespace split (the others already resolved through the alnum-normalised word or the substring lookup); 102 pass with the change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OpenWebNet-HA#793 review, 3) The comment in async_update, docs/configuration/climate.md and the test module docstring said the zones of F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 "show the humidity their probe pushes" / "the zone's own pushes keep it fresh". No physical trace shows those gateways translating a probe's humidity into *michnovka#4*Z*60*HH## on the event session (only the F461, the MyHomeServer1 and the F455 of OpenWebNet-HA#674 have dimension-60 traces). They now say only that any *michnovka#4*<WHERE>*60*<HH>## event the gateway delivers still updates current_humidity without a poll; the DEBUG line of a refused request says the same. No behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h the climate platform (OpenWebNet-HA#793 review, 4) The captured-frames section fed the F461's spontaneous *michnovka#4*2*60*38## pushes (f461_live_2026-10-23, run6) into an MH202 handler and pre-built MyHOMEClimate with humidity=True, bypassing discovery and _enable_humidity(). That test is gone from the trace section; the MH202 case is now a unit test in the "nothing to send" section with an inline frame, framed as integration-side delivery (no trace claim). The replays go through climate.async_setup_entry and async_dispatcher_send(hass, f"myhome_message_{mac}", ...) with the helpers of tests/test_climate_actuator_status.py (OpenWebNet-HA#792), each capture on the model it was recorded on, with a real handler's supports_zone_status and humidity_request_refused on the trace gateway: - F461 run6: the 171 event-session frames; the three *michnovka#4*2*60*38## pushes enable humidity on a zone configured without it (38.0), the next poll then asks *michnovka#4*2##, *michnovka#4*2*60##. Run2: *michnovka#4*2*60## ACKed with *michnovka#4*2*60*39## in 0.143 s (one echo on the event session), *michnovka#4*0*60## answered by zone 2 in 2.128 s; the echo sets 39.0. - MyHomeServer1 run2: *michnovka#4*35*60## NACKed in 2.128 s with nothing on the event session; *michnovka#4*0*60## ACKed in 0.499 s, 17 zones answer; on an 18-zone plant the 17 take their value, zone 35 stays without humidity and its poll asks *michnovka#4*35## only. Run3: *michnovka#4*39*60## ACKed with *michnovka#4*39*60*51## in 0.204 s; 51.0. - F455 (OpenWebNet-HA#674): the 67 frames on a 23-zone plant (the README's zones); the spontaneous *michnovka#4*42*60*52## enables humidity on zone 42 alone (52.0), the other 22 zones stay without, and the next poll asks *michnovka#4*42*60## and no *michnovka#4*42## (the F455 is not in the table and does not answer zone status). 103 pass (the five replays and the MH202 unit test replace the five previous trace cases). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
51aee89 to
b0bc3d1
Compare
|
Thanks, both rounds are on the branch (head b0bc3d1, rebased on 2ece474 after #792;
Full suite 4761 passed / 1 skipped at 100 % statement coverage against plain OWNd |
… (actuator status, fan speed and humidity per zone) Two physical captures behind #792 (actuator status at startup, hvac_action from dimension 20) and #793 (humidity gate), asked for in review 5642758778 of #792. f461_actuators_live_2026-10-25: a local Home Assistant at #792's head restarted with an event-only listener on the F461 2.0.11 / KW4691 zone 2 plant. The startup sweep is followed by *#4*#0*20##, answered by both actuators within 0.13 s (ACK after 2.13 s); climate.set_temperature 26.0 makes the valve report itself on 2.2 s after the dimension 12 echo (*#4*2#1*#20*1##, then *#4*2#1*20*1##), the restore to 23.5 makes it report itself off 2.1 s after the echo; the fan coil stays 5 / off. Every request is Home Assistant's, from its debug log, placed on the listener clock (source: home_assistant, t_estimated). The replay through the climate entity gives hvac_action idle -> heating -> idle and 23.5 -> 26.0 -> 23.5; the actuators attribute is asserted only where the entity exposes it. myhomeserver1_live_2026-10-25: status reads only on the MyHomeServer1 3.87.13 plant (zones 35-70): *#4*#0*20## ACKed in 0.50 s ahead of a 1.9 s burst of 43 distinct actuator frames (34 Z#1, 5 Z#2, the pumps 0#1..0#4, every one off); per zone *#4*Z*19## / *#4*Z*20## ACK on 34 zones and NACK on 54 / 56, *#4*Z*11## NACK on all 36 (the fan-speed dimension is refused on a valve zone), *#4*Z*60## ACK on the 17 zones with a probe and NACK on 19, *#4*0*60## answers the same 17; a 600 s listen with the house's own traffic. The replay of zones 35 and 47 through the climate entity gives idle with every actuator off; the pump frames have no route key and reach no zone. The 34 per-zone *#4*Z*60## reads join PHYSICAL_ONLY_TX_FRAMES (no oracle stimulus for a per-zone humidity read). Every rx frame parses in the pinned OWNd; the '#20' command forms parse as OWNHeatingCommand (OpenWebNet-HA/OWNd#138 decodes them, both are accepted). Synthetic MACs 00:03:50:00:10:25 / 00:03:50:00:10:26, documentation IPs; no log, no local path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
All requested changes from reviews #pullrequestreview-5644219403, #pullrequestreview-5644470090, and #pullrequestreview-5644539748 are cleanly resolved on b0bc3d12:
- Post-
#792Rebase & Unified Model Lookup: Rebased onto2ece4744and merges cleanly ontov2-phase1-architecture(5641f917, post-#815) with a single_model_key(re.findall(r"[A-Za-z0-9]+", ...)tokenization,KNOWN_GATEWAY_MODELS,_MODEL_SPELLINGS,_resolved_model), a single_who4_target(), unified_ZONE_REQUEST_MODELS, and merged WHO 4 release-notes bullet. - Topology Routing & Poll-Health Guard:
humidity_request_refusedfollows WHO 4 shared-bus delegation and warm-standby failover, andwants_humidityprevents entering_poll_health.should_skip(...)on refusing SCS gateways when zero frames are sent. - Strict Real-Trace Fidelity & Qualified Wording:
f461_live_2026-10-23,myhomeserver1_live_2026-10-24, andissue_674(F455) are each replayed on their own gateway family viaclimate.async_setup_entry+myhome_message_<mac>, andclimate.py/climate.mdno longer assert untraced upward humidity pushes on the 8 refusing models. - Published Oracle TSVs: OpenWebNet-HA/own-firmware-oracle#63 (
1908ccfb) is merged onmain, publishing all 12thermo-zone-requests.tsvsuites and regeneratedresults/mcp_index.json. - CI & Post-Merge Verification: 17/17 GitHub CI checks green;
ruff check,verify_ha_standards.py(54/54),sync_documentation.py --check,manage_translations.py check,typing_ratchet.py(mypy --strict: 0 error(s)), andpytestagainst both editableOWNdand pinnedOWNd==2.0.0b11pass with 100% diff coverage on the merged tree.
LGTM — merging into v2-phase1-architecture.
217b20e
into
OpenWebNet-HA:v2-phase1-architecture
… 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>
Requested by A
What is the problem
A climate zone with humidity support asks
*#4*Z*60##on every gateway after the startup sweep. In the firmware oracle'sthermo-zone-requestssuite (OpenWebNet-HA/own-firmware-oracle#60, merged 2026-10-25,findings/thermo-zone-requests.mdon main: afirmware_observedverdict on emulated firmware) the emulated firmware of eight of the twelve images (F450, F453, F453AV, F454, H4684, MH200, MH200N, MH202) NACKed the request and put nothing on the bus, so there the poll only costs a command-session round trip and a NACK in the log, while the zone's own pushes keep the value fresh. This PR skips the humidity poll on those models until the OWNd profile says otherwise. Gateways whose emulated firmware forwarded the request (F459, F460, F461, MyHomeServer1), and models the oracle does not cover, are still polled; the live F461 and MyHomeServer1 answer it.The table is an emulated-firmware verdict (
findings/thermo-zone-requests.md; the per-gateway TSV rows reach oraclemainwith OpenWebNet-HA/own-firmware-oracle#63, a mirror of the CI commit onoracle/update-suites), not a physical trace on those eight models, so it stays a fallback behind the OWNd profile flag: the landing order is this consumer, then an OWNdsupports_heating_humidity_requestflag (None= unverified) that takes over the table.Refs #712 (humidity queried at startup). No open issue is closed by this change.
What is the solution
gateway.py:humidity_request_refusedfirst asks_who4_target()(the helper of feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792, copied verbatim:_delegated_target(probe) or _failover_target(probe) or selfon a fixed WHO 4 probe, the ordersend_status_requestuses) and, when the request would go through another gateway (a secondary WHO 4 is delegated to, or the warm standby while this primary is down), returns that gateway's verdict. On the gateway itself it readsgetattr(profile, "supports_heating_humidity_request", None):True/Falsewins;Noneor a profile without the attribute (every profile of the pinned OWNd) falls back toHUMIDITY_REQUEST_REFUSED(F450, F453, F453AV, F454, H4684, MH200, MH200N, MH202), the fix(gateway): close idle command sessions before the gateway drops them #381 pattern. The flag belongs in OWNd's profiles; no OWNd issue exists for it yet.climate.py:wants_humidity = self._humidity and humidity_request_refused is not Trueis computed once and used in the early-return guard ofasync_updateand in the send branch, so a humidity zone on a refusing SCS gateway (no*#4*Z##, no fan) returns before_poll_health.should_skipinstead of counting a poll that is never sent; a refused request is logged at DEBUG._poll_on_addis unchanged (_enable_humiditystill sets it, so the first update runs after the in-band identity settled). Mock gateways without the property, and real gateways not in the table, are unchanged.gateway.py:_model_keytakes the key from a known OWNd profile's name and otherwise, for aGenericGatewayProfile, from the configured model string: a brand-prefixed alias or catalogue number is resolved word by word through OWNd's own aliases ("BTicino L4684"->H4684,"Legrand 003598"->F454,"Legrand 003565"->MH200N), the SSDP spellingHL4684(manifest.json; an SSDP identity is never overwritten by WHO 13 / 1013) is read asH4684the way fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways #794 does it, and otherwise a known model is looked up as a substring, longest first, soMH200Nstays distinct fromMH200. The lookup is keyed onKNOWN_GATEWAY_MODELS(the twelve oracle images and the other models OWNd profiles), so_model_keyof"BTicino F461"is"F461", the value feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792's tables give it.docs/configuration/climate.mdand the b16 release-notes delta say "the emulated firmware did not forward it (own-firmware-oraclefindings/thermo-zone-requests.md, afirmware_observedverdict)", not that a live gateway refuses it.Shared hunks and landing order.
_who4_target()is #792's hunk, unchanged._model_keyis a superset of #792's (theGenericGatewayProfilefallback and the longest-first substring lookup) and of #794's (HL4684->H4684, aliases throughget_gateway_profile(...).model_name);_ZONE_REQUEST_MODELSis built fromKNOWN_GATEWAY_MODELS | HUMIDITY_REQUEST_REFUSEDhere and fromACTUATOR_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS)in #792, so both give the same key for every model named in either table. The three PRs add their tables at the same place ingateway.py(and #792 / #793 touch the same release-notes line): whichever of #792 / #793 / #794 lands first, the others rebase and keep one_model_key/_ZONE_REQUEST_MODELS/_who4_target.Companion PR: none yet. The OWNd profile flag
supports_heating_humidity_request(None= unverified) should land in OWNd after oracle OpenWebNet-HA/own-firmware-oracle#60; this PR reads it forward-compatibly.State Diagram
stateDiagram-v2 direction TB state "1. Before" as Before { [*] --> Asked : sweep done, zone with humidity polls\n*#4*Z## then *#4*Z*60## on every gateway } state "2. After: humidity_request_refused" as After { [*] --> Gate : sweep done, zone polls *#4*Z## Gate --> Asked : profile flag True\nor flag None or absent and model not in HUMIDITY_REQUEST_REFUSED Gate --> Skipped : profile flag False\nor flag None or absent and model in the table , DEBUG Asked --> Fresh : *#4*Z*60## answered *#4*Z*60*HH## Skipped --> Fresh : the probes own push *#4*Z*60*HH## }The gate is evaluated on the gateway
send_status_requestroutes WHO 4 through (a delegated secondary, or the warm standby while the primary is down), not on the primary bound to the entity.Follow-up (oracle #60 merged)
findings/thermo-zone-requests.mdon oracle main instead of an unpublished run; no behaviour change. Rebased on the current base.Review follow-up (review 5644219403)
humidity_request_refusedfollows WHO 4 delegation and warm-standby failover through_who4_target()(feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792's helper, verbatim); 13 cases in both directions, two through the real topology lookup. 13 fail before._ZONE_REQUEST_MODELSkeyed onKNOWN_GATEWAY_MODELS(the twelve oracle images and the other OWNd-profiled models) united with the refusing table;assert handler._model_key == "F461"for"BTicino F461", every known model checked behind a brand prefix. 9 of the 17 brand-prefix cases fail on the previous lookup.HL4684/HC4684and brand-prefixed or catalogue strings ("BTicino L4684","Legrand 003598","Legrand 003565") resolved before the table lookup, MH200N kept distinct from MH200; fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways #794's two_model_keytests copied as they are. 12 fail before.wants_humidityin the early-return guard ofasync_update: a refusing gateway's humidity zone never reaches_poll_health; an F450 (refuses dimension 60, answers*#4*Z##) still sends and counts its zone poll. 3 fail before.MyHOMEClimate(five cases, loaders like the other trace modules); this description: checklist item 2 checked, the draft wording removed, the oracle TSV location stated.Review follow-up (reviews 5644470090 and 5644539748)
git merge-treeclean): one_model_key(this PR'sKNOWN_GATEWAY_MODELS/_MODEL_SPELLINGS/_resolved_modelsuperset now servesactuator_request_refusedandall_zone_requesttoo), one_who4_target(),_ZONE_REQUEST_MODELS = sorted(KNOWN_GATEWAY_MODELS | ACTUATOR_REQUEST_REFUSED | HUMIDITY_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS), key=len, reverse=True), the WHO 4 release-note bullet carries both clauses. feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792'stests/test_climate_actuator_status.pypasses unchanged on the result._model_keyreads the alphanumeric runs of the string (re.findall(r"[A-Za-z0-9]+", …)), so"BTicino-L4684"→H4684and"F454 (003598)"→F454; tests.climate.py,docs/configuration/climate.mdand this description no longer claim the eight gateways push humidity upward; they say that any*#4*<WHERE>*60*<HH>##event the gateway delivers still updatescurrent_humiditywithout polling.issue_674push included.oracle/update-suites(d3767f49) so thethermo-zone-requests.tsvrows andresults/mcp_index.jsonreachmain.Hardware Verification
*#4*2*60##answered with*#4*2*60*39##in 0.14 s,*#4*0*60##answered by zone 2 after 2.1 s; the thermostat pushed*#4*2*60*38##three times in 30 minutes on its own. The F461 is outside the table and keeps its poll.*#4*39*60##->*#4*39*60*51##(0.2 s);*#4*0*60##ACKed in 0.5 s and answered by 17 zones;*#4*35*60##NACKs after 2.1 s. Zone 35 never emits dimension 60 in the capture while 17 other zones do, so "no humidity probe in zone 35" is the likely reason, an inference, not a captured fact. MyHomeServer1 is outside the table; the gate is per model, not per zone, so unprobed zones still time out on gateways that forward the request.Live retest (2026-10-24)
Consensus label: gate not exercised. The F461 is outside the refused set: at startup
tx *#4*2*60##->rx *#4*2*60*41##,current_humidity: 41.0. That is a negative control (the request is still sent where it should be), not a test of the skip.How is it tested
tests/test_climate_humidity_poll_gate.py(97 cases). The original 36: the fallback table for 15 models (including F455, MH201 and Generic, which are not in the oracle and are still asked); the profile flagTrue/Falseoverriding the table both ways;None= unverified (×5) and a profile without the attribute (×2) falling back to the table; an F455 zone with the flagNonestill sends*#4*2*60##; an F461 zone still sends*#4*2##,*#4*2*60##; F454 and MH202 zones send nothing; a gateway without the property is still asked;_model_keyforBTicino F461,Legrand MH200N,MH200,BTicino F453AV,Unknown,Generic, and aLegrand MH200Nzone sending nothing.test_aliased_model_strings_follow_their_profile(×4) andtest_ssdp_model_string_hl4684_is_keyed_as_h4684, eleven brand-prefixed / catalogue / SSDP strings and two H4684 humidity zones (HL4684,BTicino L4684) not asking for humidity (18);_poll_healthnever reached on a refusing SCS gateway (×3), the F450 zone poll still counted, the F461 zone through_poll_healthand held back when it skips,_poll_on_addafter_enable_humidity(7); the captured frames (5, below)._model_keycases fail on the helper before 7ec1f1f; of the new cases 13 (delegation), 9 (brand-prefixed forwarding models), 12 (SSDP / aliases) and 3 (_poll_health) fail on the code before each commit; all 97 pass with the change.master, f04d8aa, onPYTHONPATH): 4761 passed, 1 skipped, 100.0 % statement coverage (16358 statements), onv2-phase1-architecture2ece474 (after feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792).ruff check .(0.17.2 and 0.16.7),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). Physical traces exist for the positive side only.tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, runsrun2_reviewer_reads(requests) andrun6_event_session_ha_fixed_integration(pushes)*#4*2*60##->*#4*2*60*39##(0.14 s);*#4*0*60##->*#4*2*60*39##(2.1 s); spontaneous*#4*2*60*38##×3tests/fixtures/traces/myhomeserver1_live_2026-10-24/myhome_trace_MyHomeServer1_live_2026-10-24.json, runsrun2_reads_thermo_sound_alarm_lightsandrun3_reads_lights_addresses_humidity*#4*35*60##NACK 2.1 s;*#4*0*60##ACK 0.5 s with 17 zone replies (*#4*39*60*51##…*#4*70*60*69##);*#4*39*60##->*#4*39*60*51##findings/thermo-zone-requests.md, merged in OpenWebNet-HA/own-firmware-oracle#60)tests/test_climate_humidity_poll_gate.pyreplays each capture on its own gateway model throughclimate.async_setup_entryandmyhome_message_<mac>(the pattern oftests/test_climate_actuator_status.py): F461run6spontaneous*#4*2*60*38##pushes enable humidity and the zone is then asked, F461run2*#4*2*60##→*#4*2*60*39##; MyHomeServer1run2zone 35*#4*35*60##NACK and the*#4*0*60##17-zone burst,run3zone 39*#4*39*60##→*#4*39*60*51##; and the F455 oftests/fixtures/traces/issue_674/myhome_trace_f455_climate_timeouts_2026-10-09.json, an SCS gateway outside the table, whose spontaneous*#4*42*60*52##push updates the zone while it is still polled. No capture of one gateway is presented as another's traffic; the MH202 case (a delivered event enables humidity, the poll stays off) is a unit test of integration-side delivery.What does the Firmware Oracle / Encyclopedia say
functional/who-4-temperature-control/dimensions.md): dimension60is not in the published functionalDIMENSIONtable (0,11,12,13,14,19,20,22,30,31); humidity appears only in the historical BACnet air-handling-unit record. The request and its*#4*Z*60*HH##reply are known from captures (feat(climate): remember zone humidity support and query dimension 60 on startup (#710) #712 and the two plants above), not from the specification.findings/thermo-zone-requests.md(feat(oracle): add thermo-zone-requests suite (per-zone polls, actuators, humidity, interface form) own-firmware-oracle#60, merged 2026-10-25):*#4*2*60##and*#4*0*60##nack, silent on F450020010, F453010100, F453AV030014, F454020051, H4684020054, MH200020102, MH200N010108, MH202010024; nack, out on F459020105, F460020012, F461020011, MyHomeServer1028206. The per-gateway rowsresults/<P>/<V>/oracle/full/thermo-zone-requests.tsvand the regeneratedresults/mcp_index.jsonwere produced by thesuites.ymlrun for52d354bband pushed to the oracle branchoracle/update-suites(commit d3767f49); they reachmainwith the maintainers' next suites PR, so no published TSV row onmaincovers dimension 60 yet (none at 53f9514). The oracle runs emulated firmware with no probe behind it, so it shows whether the request reaches the bus, not whether a probe answers or pushes.Checklist
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).handle_event()and do not manually subscribe viaasync_dispatcher_connect.pytest,ruff, andscripts/verify_ha_standards.pypass.🤖 Generated with Claude Code