Repository navigation
fix(climate): gate unrouted WHO 4 #4#II and actuator status polls - #816
Draft
GreenGrassBlueOcean wants to merge 3 commits into
Draft
GreenGrassBlueOcean wants to merge 3 commits into
GreenGrassBlueOcean wants to merge 3 commits into
Conversation
This was referenced Oct 26, 2026
GreenGrassBlueOcean
force-pushed
the
fix/who4-interface-routing-and-actuator-guards
branch
from
October 26, 2026 09:57
5370178 to
d28507e
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
GreenGrassBlueOcean
marked this pull request as draft
October 26, 2026 16:15
GreenGrassBlueOcean
force-pushed
the
fix/who4-interface-routing-and-actuator-guards
branch
from
October 26, 2026 16:46
32f4e6b to
2e3c5ee
Compare
gdluck
pushed a commit
to gdluck/HomeAssistant-MyHOME
that referenced
this pull request
Oct 26, 2026
…grammar (OpenWebNet-HA#792 follow-up)" This reverts commit 26c4e63f3b0c3c2bd1dc26dd1b0f1b0b2c7e0ba1 at the maintainer's request on OpenWebNet-HA#821: the validator change had a `.partition` bug on an actuator `michnovka#4` behind a one-digit interface (`2#4#4#1`), it lags OWNd#148's bounds, and OpenWebNet-HA#816 already rewrites the same lines (with `.rpartition` and bounded digits); the `.fullmatch` catch for "2\n" and the OWNd#148 bounds will be folded into OpenWebNet-HA#816 instead. A revert rather than a history rewrite so the branch keeps its pushed commits; the tree equals the branch without that commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 26, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the problem
Important
Merge-order blocker (held in Draft): Keep this PR in Draft until upstream protocol & oracle evidence PRs
OpenWebNet-HA/OpenWebNet-Encyclopedia#52andOpenWebNet-HA/own-firmware-oracle#65are merged tomain.Rebased onto
v2-phase1-architecture(f4a5a907, incorporating#794,#818, and#808), severalWHO = 4(thermoregulation) polling, outbound-write, event-echo, andWHEREgrammar surfaces remained unsafe or misleading against the raw SCS bus frames recorded across all 12 emulated gateway firmwares inOpenWebNet-HA/own-firmware-oracle#65(8e85216cd8a30e3b51d89a0217528f639ee2bcef):#4#IIWHO = 4frames (*#4*Z#4#II##,*#4*#0#4#II##,*0##,*11##,*19##,*20##,*60##, outbound writes, and event-session Dimension14echoes) are not routed acrossF422by any of the 12 emulated gateway firmwares:#794(b48e774e) introducedINTERFACE_ZONE_REQUEST_REFUSEDand_interface_poll_refused, but relied onverdict=outon*#4*2#4#01*0##to exemptH4684,MH200,MH201,F455, andGeneric, gated_interface_poll_refusedinclimate.pyonself._temperature_poll_wantedandself._interface is not None, and left outbound writes (async_set_hvac_mode,async_set_temperature,async_set_fan_mode) and event-session Dimension14echoes (gateway_events.py) stripping#4#IIonto local main-bus zoneZ.scs_outbytes inthermo-zone-requests.tsv(8e85216c) shows zero$06EC.../$06E4...interface-routed telegrams on any target:H4684(2.0.54) andMH200(2.1.2) parse the leading integer before#and emit unrouted main-bus zone-2queries ($0499023000\r,$0499023012\r,$049902300E\r,$049902300D\ron status;$0499023000\ron*0##;$0499023013\ron*19##), cross-contaminating4-Z#4#IIwith local main-bus zoneZ.F450(2.0.10),F454(2.0.51),F459(2.1.5),F460(2.0.12),F461(2.0.11), andMyHomeServer1(2.82.6) truncateWHEREat#on*20##($04A9023010\r/$004A9023010\r) and*60##($06D102030308000000\r/$006D102...), cross-polling local main-bus zoneZ.async_set_hvac_mode,async_set_temperature,async_set_fan_mode) builtOWNHeatingCommand.*(self._where, ...)withoutself._interface, silently writing to local main-bus zoneZor#0when triggered on a4-Z#4#II/4-#0#4#IIentity; similarly,gateway_events.pystripped#4#IIfrom incoming*#4*Z#4#II*#14*...##echoes and emitted unrouted*#4*Z##(or invalid*#4*0##on#0/#0#1)._interface_poll_refusedtoself._temperature_poll_wanted(and excluding central#0#4#IIor inlinezone: "2#4#01") leftF459cross-polling*#4*Z#4#II*60##onto main-bus zoneZ, leftF460/F461/MyHomeServer1/Genericpolling dimensionless*#4*Z#4#II##(silent+NACK-> falseunresponsiverepair issues), and exposedMH202(1.0.24) while configured asGenericbefore SSDP/UPnP orWHO 13model identification (*#4*2#4#01##misparses as zone4, actuator1and crashescosowithSIGSEGV).Z#N,Z#0,0#N,0#0,3#Z#N,Z#N#4#II) and 4-zone central unit channels (#0#1..#0#4):MH202(1.0.24), replayed dimensionless*#4*2#1##and*#4*2#4#01##crashcosowithSIGSEGV(verdict=crash), and static disassembly oflibthermo.so(device_recognitionat0x18c4) +libplug_thermo_tr.so(thermo_open2scs+0x394at0x22b4,g_list_first(NULL)->data) shows non-#3-tokenA#B#C(1 <= B <= 99,1 <= C <= 9, e.g.3#2#1) enters the same local-actuator branch.*0##,*11##,*19##, and*60##are either rejected (nack, silent) or truncate at#onto local zoneZ, whereas per-actuator valve status uses Dimension20(*#4*Z#N*20##or*#4*#0*20##).#0#2..#0#4(BTicino 4695) were not treated symmetrically with#0#1inclimate.py(build(),__init__,async_update), andknown_keys/_zone_configstripped internal#characters viazone_number("#0#2") -> "02"(deduplicating local zone"02").Related:
#794,OpenWebNet-HA/OWNd#140,OpenWebNet-HA/own-firmware-oracle#65,OpenWebNet-HA/openwebnet-mcp#22,OpenWebNet-HA/OpenWebNet-Encyclopedia#52What is the solution
is_actuator_where(where)incustom_components/myhome/where_grammar.py:#4#IIinterface suffix (rpartition("#4#")with1 <= len(iface) <= 2) and identifies 2-token actuator / all-actuators / pump addresses (Z#N,Z#0,0#N,0#0with0 <= N <= 9) and 3-token split / actuator addresses (3#Z#N/A#B#Cwith0 <= C <= 9, including#0#0,#0#9,Z#N#4#II, and3#Z#N#4#II), while excluding plain zones (Z), routed zones (Z#4#II), bare#4#II, out-of-range actuator tails (2#10,#0#99,2#4#100), and central units (0,#0,#0#1..#0#4,#0#4#II).#4#IInormalization (discovery.py,climate.py), bound_ZONE_WHERE(gateway.py), and guard Dimension14echoes (gateway_events.py):#4#<1..2 digits>viarpartition(BUS_ROUTING)only whenbaseis non-empty and1 <= len(iface) <= 2inAddress.from_device_id,Address.from_config, andMyHOMEClimate.__init__so inlinezone: "2#4#01"(or2#4#4#1) normalizes identically towhere="2", interface="01"("2#4#4#01") without turning bare"#4#01"intowhere=""or accepting 3-digit"2#4#100"._ZONE_WHEREingateway.pyto^#?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})?$(matchingOWNd#1400..99zone and0..9actuator bounds) and normalize 1-digit interface suffixes viarpartition(BUS_ROUTING)in_validate_zone_where.INTERFACE_ZONE_REQUEST_REFUSEDfrozenset from#794with a fleet-wideTruedefault inMyHOMEGatewayHandler.interface_zone_request_refused(delegating through_who4_target()while honoring explicitprofile.supports_interface_zone_requests), and gateBUS_ROUTING in wherein_send_zone_requests.OWNHeatingCommandDimension14handler ingateway_events.pyso#4#IIechoes are withheld wheninterface_zone_request_refusedisTrue(or preserve#4#IIwhenFalse), and#0/#0#1..#0#4/Z#Nechoes never emit*#4*0##or cross-poll zoneZ.#4#IIpolls & writes, actuator polls, and#0#1..#0#44-zone central channels incustom_components/myhome/climate.py:_interface_poll_refused = ((self._interface is not None or BUS_ROUTING in self._full_where) and gateway.interface_zone_request_refused is True)across routed zones (Z#4#II), routed central units (#0#4#II), and malformed inline#4#...strings regardless of_temperature_poll_wanted.*#4*...##), temperature (*0##), fan (*11##), humidity (*60##), and_request_zone_dimension(*11##,*19##) when_interface_poll_refusedisTrue(with a one-timeDEBUGlog when a poll was wanted), and refuse outbound writes (async_set_hvac_mode,async_set_temperature,async_set_fan_mode) via_refuse_interface_write()with aWARNINGlog instead of stripping#4#IIonto local main-bus zoneZor#0.is_actuator_where(self._where)from_poll_on_add,_polls_temperature,_enable_fan_mode,_enable_humidity,async_update, and_request_zone_dimension, and guardwatch_zone_actuatorswith_ZONE_WHERE.match(self._full_where).#0#1..#0#4(_FOUR_ZONE_CENTRAL) uniformly asCentral Unit (4695)(_central = True, never polled) and guardknown_keys/_zone_configagainst stripping internal#tokens (zone_number("#0#2") -> "02"no longer collides with local zone"02").docs/configuration/climate.md,.github/release-notes/2.0.0b16.md):#4#IIpoll and write withholding, the3#4#1..3#4#91-digit shorthand normalization, and actuator/split poll guards.State Diagram
flowchart TD Start["Climate poll / write / Dim 14 echo"] --> Routed{"#4#II routed AND gateway.interface_zone_request_refused?"} Routed -- "Yes (all 12 oracle models + Generic by default)" --> Withhold["Withhold WHO 4 polls (*#4*...##, *0..*60##), Dim 14 echo polls, and outbound writes (passive bus events still apply)"] Routed -- "No (main bus or profile override)" --> Actuator{"is_actuator_where(_where)? (Z#N, Z#0, 0#N, 3#Z#N)"} Actuator -- "Yes" --> GuardActuator["Suppress *#4*W## (MH202 SIGSEGV), *0##, *11##, *19##, *60##, and Dim 14 echo polls; allow 2-token *#4*Z#N*20## watch when unrouted"] Actuator -- "No" --> Central{"_central? (#0, 0, #0#1..#0#4)"} Central -- "Yes (#0#1..#0#4)" --> Skip4695["4695 channel: event-driven only, never poll status/dims"] Central -- "Yes (#0 / 0)" --> PollCentral["3550 central: allow *#4*#0## status request (skip Dim 14 *#4*0##)"] Central -- "No (zone Z in 1..99)" --> PollZone["Emit supported gateway polls (*0##, *11##, *19##, *20##, *60##, or *#4*Z##) and zone writes"]Hardware Verification
OpenWebNet-HA/own-firmware-oracle#65(8e85216cd8a30e3b51d89a0217528f639ee2bcef).How is it tested
tests/test_startup_requests_per_firmware.py:test_interface_zone_is_not_polled_across_any_unrouted_gateway,test_ssdp_model_string_hl4684_is_keyed_as_h4684, &test_f455_zone_still_sends_nothing: verify acrossF450,F454,F459,MH202,H4684,HL4684,H/L4684,MH200,MH201,F460,F461,MyHomeServer1,F455, andGenericthat routed#4#01zones withhold startup/periodic polls (_poll_on_add is False), emit zero frames, and log once atDEBUGonly when a poll would otherwise have been sent.test_withheld_interface_poll_also_withholds_humidity_and_fan_cross_polls: parametrized acrossF459(humidity=True),F453,F453AV, andMH200N(humidity=False), confirming both*60##and*11##cross-polls are withheld and logged once.test_profile_flag_wins_over_the_interface_zone_default: verifiesprofile.supports_interface_zone_requests(True/False) overrides the fleet default for both*0##and status-only (F461) gateways.tests/test_climate_actuator_status.py:test_interface_zone_request_refused_defaults_true_across_fleet&test_interface_zone_request_refused_respects_profile_flag_and_delegation: verify the fleet-wideTruedefault across all 12 oracle models +Genericand delegation via_who4_target().test_an_invalid_zone_address_is_refused_at_once,test_routed_zone_is_withheld_by_default_and_asked_when_profile_opts_in, &test_a_one_digit_interface_is_normalized_to_two_digits_and_gated: verify bounded_ZONE_WHERErejection (105,001,2#10,#4#1,#4#01,3#2#1),rpartitionnormalization (2#4#4#1->2#4#4#01), and_send_zone_requests#4#IIgating.test_routed_zone_on_generic_or_f461_never_polls_status_or_raises_unresponsive: verifiesGenericandF461emit zero frames on4-2#4#01without raisingunresponsiverepairs.test_inline_routed_where_and_routed_central_withhold_polls_and_writes: verifies inlinewhere="2#4#1"and routed central#0#4#01emit zero polls acrossGeneric,F461,F459,H4684, andMH202, log one-shotDEBUGfor both entities, and refuseasync_set_hvac_mode,async_set_temperature, andasync_set_fan_modewithWARNINGlogs (allowing writes through whensupports_interface_zone_requests=True).test_is_actuator_where_distinguishes_actuators_from_central_and_routed&test_actuator_where_never_sends_status_or_zone_dimension_polls: verifyis_actuator_whereacross all actuator/central/routed/out-of-range boundaries (#0#0,#0#9,#0#1..#0#4,#0#99,2#10,2#4#100,3#4#1,"","4-") and confirm zero*#4*W##,*0##,*11##,*19##, or*60##frames are emitted.test_four_zone_central_channels_1_to_4_are_central_and_never_polled&test_four_zone_central_configured_channels_use_4695_model_and_skip_polls: verify#0#1..#0#4getCentral Unit (4695),_central=True, zero polls, and zero deduplication collision with local zone"02".test_malformed_inline_routing_does_not_crash_added_to_hass_or_poll&test_gateway_events_dim14_echo_guards_routed_central_and_actuator_addresses: verify malformed#4#strings (2#4#100,3#4#001,#4#01) do not crashasync_added_to_hassand Dimension14event echoes do not cross-poll local zoneZor emit*#4*0##.tests/test_platform_discovery.py:test_address_forms_follow_openwebnet_where_conventions: verifiesAddress.from_device_idandAddress.from_configsplit valid2#4#4#01/2#4#1while preserving malformed#4#01and2#4#100unsplit.pytest tests -q -o addopts="" --cov=custom_components/myhome --cov-report=term-missing:5,039 passed,100.0%coverage (16,879statements,0missed).OWNd==2.0.0b11check:scripts/typing_ratchet.py(mypy --strict: 0 error(s) in 0 module(s); baseline 0) and293 passedacross the modified test modules.ruff check custom_components tests scripts: All checks passed (0violations).scripts/verify_ha_standards.py: PASSED (54/54Platinum quality scale rules satisfied/exempt).scripts/sync_documentation.py --check: PASSED (0broken links, oracle consistency verified).Where are the traces
OpenWebNet-HA/own-firmware-oracle#65(8e85216cd8a30e3b51d89a0217528f639ee2bcef):findings/thermo-zone-requests.mdandresults/<product>/<version>/oracle/full/thermo-zone-requests.tsvacross all 12 emulated gateway targets (F450 020010withreset=each;F453 010100,F453AV 030014,F454 020051,F459 020105,F460 020012,F461 020011,H4684 020054,MH200 020102,MH200N 010108,MH202 010024, andMyHomeServer1 028206withreset=0).What does the Firmware Oracle / Encyclopedia say
WHO = 4,OpenWebNet-Encyclopedia#52):functional/who-4-temperature-control/addressing.mdandprotocol/addressing.mdseparate publishedWHO = 4#4#INTERFACEsyntax from observed gateway translation plugin behavior:#4#INTERFACEforwarding is implemented per-WHOin gateway translation plugins rather than uniformly across subsystems, and per-actuator queries use Dimension20(*#4*Z#N*20##/*#4*#0*20##) rather than dimensionless*#4*Z#N##(ownkb:claim:c003279,ownkb:claim:c003280).own-firmware-oracle#65,openwebnet-mcp#22):$06EC.../$06E4...local-bus-routed SCS frames on any testedWHO = 4#4#IIrequest (2#4#01,2#4#12,*0##,*11##,*19##,*20##,*60##,2#4#12*20##,2#1#4#01*20##).verdict=crashrows onMH202 010024: dimensionless*#4*2#1##and*#4*2#4#01##terminatecosowithSIGSEGVinlibplug_thermo_tr.so(thermo_open2scs+0x394at0x22b4,g_list_first(NULL)->data).MH202 010024:libthermo.so(device_recognitionat0x18c4) classifies non-#3-tokenA#B#Cwith1 <= B <= 99and1 <= C <= 9(such as3#2#1) into the same local-actuator code path (actuator B#C), sois_actuator_whereguards 3-tokenA#B#Calongside 2-tokenZ#N.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.