Repository navigation
feat(heating,profiles): add WHO 4 .actuator, #4#IF disambiguation, and profile capability flags - #164
Conversation
…biguation, and profile flags
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@gdluck can you please review this one. and please make changes directly in this pr or make me a new pr for this! Many thanks Laurens |
|
Yes, allow me few minutes. |
|
Review of 1. Parsing, verified on master vs this branchI parsed the same frames with
Master already decodes actuator 4 correctly on the plain 2. When the two halves actually occur
3.
|
…ed models Oracle rows in trace-corpus.tsv show F453, F453AV, and MH200N emit the 4 SCS telegrams for *OpenWebNet-HA#4*1## identically to F454 rather than remaining silent, while F455 has no WHO 4 oracle rows. Leave supports_heating_zone_status as None (unverified) on F453, F453AV, F455, and MH200N so caller pacing policy continues to decide until live traces exist.
|
Thanks @gdluck! Great catch on the oracle rows for F453, F453AV, and MH200N in I've pushed commit
All 2,828 tests pass and linters / documentation sync checks are clean. |
|
@gdluck can you please rebase this one as well please |
|
The head branch of this PR is on your fork ( @@ -360,15 +360,17 @@ class OWNHeatingEvent(OWNEvent):
self._type = None
where = self._where or "0"
+ _, where_params = self._interface_and_where_params()
# The frame grammar admits the wildcard WHERE ``*`` for every WHO
# (``*4*1**##``); it is no zone, so the event is zone 0, as a frame
# without a WHERE already was, instead of raising ValueError from int().
zone_digits = where[1:] if where.startswith("#") else where
self._zone = _ascii_int(zone_digits) or 0
# ``#0#N`` is zone N of a 4-zone central unit; ``0#N`` is actuator N
- # of zone 0 (a pump the zones share), not zone N.
- if self._zone == 0 and self._where_param and where.startswith("#"):
- self._zone = int(self._where_param[0])
+ # of zone 0 (a pump the zones share), not zone N; ``#0#4#IF`` is ``#0``
+ # behind interface ``IF`` (``where_params`` is empty).
+ if self._zone == 0 and where_params and where.startswith("#"):
+ self._zone = int(where_params[0])
self._sensor = None
if self._zone > 99:The PR's Checked on the merged result ( Generated by Claude Code |
|
@gdluck can you please make a new pr than i will close this one |
|
New PR opened: #168 — your three commits ( Generated by Claude Code |
What is the problem
Downstream WHO 4 consumers in
OpenWebNet-HA/MyHOMEencountered two protocol-layer gaps surfaced in MyHOME#863 / MyHOME#864 and MyHOME#866:#4(Z#4) vs.F422bus interface (#4#IF) disambiguation & missing.actuatorproperty (MyHOME#863):OWNHeatingEventstored Dimension 20 actuator indices only on the private attributeself._actuatorwithout a public@property def actuator, and misreadZ#4#IF(_where_param == ["4", "01"]) as actuator"4"instead of implicit actuator"1"on interface"01".OWNHeatingCommandlacked.actuatoron Dimension 20 requests (*#4*Z#N*20##) andMyHomeServer1Dimension 20 writes (*#4*Z#N*#20*VAL##).OWNMessage.interfaceomittedWHO = 4, forcing downstream callers to re-slicewhere_paramby hand and causing30#4(Zone 30, Actuator 4) to collide with#4#IFchecks.GatewayProfile(MyHOME#866):MyHOMEalready queriesprofile.supports_general_heating_status,profile.supports_heating_actuator_status,profile.supports_heating_humidity_request, andprofile.supports_interface_zone_requests(plussupports_heating_zone_statusfor*#4*Z##polling), butGatewayProfiledid not yet declare these five fields, leaving model-string fallback tables as the sole source of truth and keepingF454(003598) misclassified despite both central-unit (MyHOME#629, FW1.0.34) and standalone-thermostat (MyHOME#866, FW2.0) live traces provingF454answers all 4 expandedbt_H4690SCS bus queries on*#4*Z##in ~1.3 s across its 4 command sessions.What is the solution
OWNMessage._interface_and_where_params()&OWNMessage.interface/event_content(OWNd/message/base.py):OWNMessage.interfacetoWHO = 4whendimension != 22(keeping split-unit3#Z#Naddresses such as*#4*3#4#1*22##unrouted):len(_where_param) == 2 and _where_param[0] == "4"(Z#4#IF,#0#4#IF) -> interface_where_param[1], remaining[].len(_where_param) == 3 and _where_param[1] == "4"(Z#N#4#IF,#0#C#4#IF) -> interface_where_param[2], remaining[_where_param[0]].OWNMessage.event_contentto emit"where parameters"from the remaining non-interface tokens so30#4#4#01preserves["4"]alongside"interface": "01".OWNHeatingEvent.actuator&OWNHeatingCommand.actuator(OWNd/message/heating.py):@property def actuator(self) -> str | Noneon bothOWNHeatingEvent(Dimension 20 replies) andOWNHeatingCommand(Dimension 20 requests and#20writes), derived from the non-interfacewhere_params("4"for30#4and30#4#4#01,"1"for30and30#4#01).#0#4(where_params == ["4"]) resolved to 4-zone central unit4, while#0#4#01(where_params == [],interface == "01") stays central unit#0on interface"01".GatewayProfilecapability flags (OWNd/profiles.py):bool | None = Nonefields (None= unverified):supports_general_heating_status(*#4*0##):TrueonF453,F453AV,MH200N;FalseonF454,F461,H4684,MH200,MH201,MH202,MyHomeServer1.supports_heating_zone_status(*#4*Z##):TrueonF454,F461,MH202,MyHomeServer1(promoted via live traces and oracleACK/OUT); leftNoneonF453,F453AV,F455,MH200N, and single-session / unmeasured models so caller pacing policy continues to decide until live-traced.supports_heating_actuator_status(*#4*Z*20##):TrueonF454,F461,MyHomeServer1;FalseonF453,F453AV,H4684,MH200,MH200N,MH202.supports_heating_humidity_request(*#4*Z*60##):TrueonF461,MyHomeServer1;FalseonF453,F453AV,F454,H4684,MH200,MH200N,MH202.supports_interface_zone_requests(*#4*Z#4#II...##):Falseon all nine profiled gateways in the 12-image oracle (F453,F453AV,F454,F461,H4684,MH200,MH200N,MH202,MyHomeServer1).State Diagram
flowchart LR W["WHO=4 Frame WHERE"] --> D22{"dimension == 22?"} D22 -- "Yes (3#Z#N split)" --> NoIf["interface = None\nwhere_params = _where_param"] D22 -- "No" --> Len{"_where_param shape"} Len -- "['4', IF] (Z#4#IF)" --> IfOnly["interface = IF\nwhere_params = []\nDim 20 actuator = '1'"] Len -- "[N, '4', IF] (Z#N#4#IF)" --> ActIf["interface = IF\nwhere_params = [N]\nDim 20 actuator = N"] Len -- "[N] (Z#N, e.g. 30#4)" --> ActOnly["interface = None\nwhere_params = [N]\nDim 20 actuator = N"] Len -- "[] (bare Z)" --> Bare["interface = None\nwhere_params = []\nDim 20 actuator = '1'"]Hardware Verification
F454FW2.0standalone-zone capture (MyHOME#866,@xduss): 13 standalone room thermostats (zones 1..13, zero#0central unit) answer*#4*Z##in1.31–1.33 swith all 5 frames (*#4*Z*0*TTTT##,*#4*Z*12*TTTT*3##,*4*1*Z##,*#4*Z*14*0200*3##,*#4*Z*13*00##).MH202FW1.0standalone-zone capture (MyHOME#866,@xduss): 5 standalone room thermostats (zones 1..5) answer*#4*Z##in160–250 mswith all 5 frames; confirmed by oracleMH202 010024returningACK (*#*1##)+OUT (Bus).F454FW1.0.34+3550#0capture (MyHOME#629):*#4*1##..*#4*15##answered in ~1.3 s each.MyHomeServer1multi-actuator zone (MyHOME#863,@andreasorbelli): Zone30actuator#4(*#4*30#4*20*0/1##,*#4*30#4*#20*0/1##).How is it tested
tests/test_who4_actuators_status.py: addedtest_who4_dim20_actuator_and_interface_disambiguation,test_who4_command_actuator_and_dim22_split_non_collision, andtest_who4_non_dim20_events_and_central_interface_disambiguation.tests/test_profiles.py: addedtest_who4_heating_capability_flagscovering all 5 WHO 4 flags across all canonical profiles and catalog aliases (003598,MHS1,H/L4684,Generic).ruff check OWNd tests scriptsclean,scripts/sync_documentation.py --checkandscripts/update_readme_profiles.py --checkin sync.Where are the traces
OpenWebNet-HA/MyHOME:tests/fixtures/traces/issue_629/,tests/fixtures/traces/issue_674/,tests/fixtures/traces/f461_live_2026-10-23/,tests/fixtures/traces/myhomeserver1_live_2026-10-24/,tests/fixtures/traces/issue_466/, and companion PR fortests/fixtures/traces/issue_866/.What does the Firmware Oracle / Encyclopedia say
who-4-temperature-control,ownkb:chunk:r000185):WHERE = Z#N(Z = 1..99,N = 1..9) addresses actuatorNof zoneZ;#4#IFroutes across local bus interfaceIF; Dimension 22 split control uses3#Z#N.thermo-zone-requests,thermo-actuator,trace-corpus):*#4*0##:ack/outonF453,F453AV,MH200N;nack/silentonF450,F454,F459,F460,F461,H4684,MH200,MH202,MyHomeServer1.*#4*1##:out($0499013000,3012,300E,300D) onF454 020051;outonF461 020011andMyHomeServer1 028206;ack/outonMH202 010024;nack/out(byte-identical 4 SCS telegrams to F454) onF453,F453AV,MH200N(leftNoneas unverified until live multi-session heating traces exist).*#4*1*20##:outonF454,F461,MyHomeServer1;nack/silentonF453,F453AV,H4684,MH200,MH200N,MH202.*#4*1*60##:outonF461,MyHomeServer1;nack/silentonF453,F453AV,F454,H4684,MH200,MH200N,MH202.*#4*2#4#01...##: zero of twelve emulated firmwares emit$06EC/$06E4interface-routed WHO 4 bus frames.