Skip to content

feat(heating,profiles): add WHO 4 .actuator, #4#IF disambiguation, and profile capability flags - #164

Closed
GreenGrassBlueOcean wants to merge 3 commits into
OpenWebNet-HA:masterfrom
GreenGrassBlueOcean:feat/who4-actuator-interface-and-profile-flags
Closed

GreenGrassBlueOcean wants to merge 3 commits into
OpenWebNet-HA:masterfrom
GreenGrassBlueOcean:feat/who4-actuator-interface-and-profile-flags

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What is the problem

Downstream WHO 4 consumers in OpenWebNet-HA/MyHOME encountered two protocol-layer gaps surfaced in MyHOME#863 / MyHOME#864 and MyHOME#866:

  1. Actuator #4 (Z#4) vs. F422 bus interface (#4#IF) disambiguation & missing .actuator property (MyHOME#863):
    • OWNHeatingEvent stored Dimension 20 actuator indices only on the private attribute self._actuator without a public @property def actuator, and misread Z#4#IF (_where_param == ["4", "01"]) as actuator "4" instead of implicit actuator "1" on interface "01".
    • OWNHeatingCommand lacked .actuator on Dimension 20 requests (*#4*Z#N*20##) and MyHomeServer1 Dimension 20 writes (*#4*Z#N*#20*VAL##).
    • OWNMessage.interface omitted WHO = 4, forcing downstream callers to re-slice where_param by hand and causing 30#4 (Zone 30, Actuator 4) to collide with #4#IF checks.
  2. Missing declarative WHO 4 request capability flags on GatewayProfile (MyHOME#866):
    • MyHOME already queries profile.supports_general_heating_status, profile.supports_heating_actuator_status, profile.supports_heating_humidity_request, and profile.supports_interface_zone_requests (plus supports_heating_zone_status for *#4*Z## polling), but GatewayProfile did not yet declare these five fields, leaving model-string fallback tables as the sole source of truth and keeping F454 (003598) misclassified despite both central-unit (MyHOME#629, FW 1.0.34) and standalone-thermostat (MyHOME#866, FW 2.0) live traces proving F454 answers all 4 expanded bt_H4690 SCS bus queries on *#4*Z## in ~1.3 s across its 4 command sessions.

What is the solution

  1. OWNMessage._interface_and_where_params() & OWNMessage.interface / event_content (OWNd/message/base.py):
    • Extend OWNMessage.interface to WHO = 4 when dimension != 22 (keeping split-unit 3#Z#N addresses 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]].
    • Update OWNMessage.event_content to emit "where parameters" from the remaining non-interface tokens so 30#4#4#01 preserves ["4"] alongside "interface": "01".
  2. OWNHeatingEvent.actuator & OWNHeatingCommand.actuator (OWNd/message/heating.py):
    • Expose public @property def actuator(self) -> str | None on both OWNHeatingEvent (Dimension 20 replies) and OWNHeatingCommand (Dimension 20 requests and #20 writes), derived from the non-interface where_params ("4" for 30#4 and 30#4#4#01, "1" for 30 and 30#4#01).
    • Keep #0#4 (where_params == ["4"]) resolved to 4-zone central unit 4, while #0#4#01 (where_params == [], interface == "01") stays central unit #0 on interface "01".
  3. Declarative WHO 4 GatewayProfile capability flags (OWNd/profiles.py):
    • Add five bool | None = None fields (None = unverified):
      • supports_general_heating_status (*#4*0##): True on F453, F453AV, MH200N; False on F454, F461, H4684, MH200, MH201, MH202, MyHomeServer1.
      • supports_heating_zone_status (*#4*Z##): True on F454, F461, MH202, MyHomeServer1 (promoted via live traces and oracle ACK/OUT); left None on F453, 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##): True on F454, F461, MyHomeServer1; False on F453, F453AV, H4684, MH200, MH200N, MH202.
      • supports_heating_humidity_request (*#4*Z*60##): True on F461, MyHomeServer1; False on F453, F453AV, F454, H4684, MH200, MH200N, MH202.
      • supports_interface_zone_requests (*#4*Z#4#II...##): False on 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'"]
Loading

Hardware Verification

  • Live F454 FW 2.0 standalone-zone capture (MyHOME#866, @xduss): 13 standalone room thermostats (zones 1..13, zero #0 central unit) answer *#4*Z## in 1.31–1.33 s with all 5 frames (*#4*Z*0*TTTT##, *#4*Z*12*TTTT*3##, *4*1*Z##, *#4*Z*14*0200*3##, *#4*Z*13*00##).
  • Live MH202 FW 1.0 standalone-zone capture (MyHOME#866, @xduss): 5 standalone room thermostats (zones 1..5) answer *#4*Z## in 160–250 ms with all 5 frames; confirmed by oracle MH202 010024 returning ACK (*#*1##) + OUT (Bus).
  • Live F454 FW 1.0.34 + 3550 #0 capture (MyHOME#629): *#4*1##..*#4*15## answered in ~1.3 s each.
  • Live MyHomeServer1 multi-actuator zone (MyHOME#863, @andreasorbelli): Zone 30 actuator #4 (*#4*30#4*20*0/1##, *#4*30#4*#20*0/1##).

How is it tested

  • tests/test_who4_actuators_status.py: added test_who4_dim20_actuator_and_interface_disambiguation, test_who4_command_actuator_and_dim22_split_non_collision, and test_who4_non_dim20_events_and_central_interface_disambiguation.
  • tests/test_profiles.py: added test_who4_heating_capability_flags covering all 5 WHO 4 flags across all canonical profiles and catalog aliases (003598, MHS1, H/L4684, Generic).
  • Full suite: 2,828 passed, ruff check OWNd tests scripts clean, scripts/sync_documentation.py --check and scripts/update_readme_profiles.py --check in 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 for tests/fixtures/traces/issue_866/.

What does the Firmware Oracle / Encyclopedia say

  • OpenWebNet Encyclopedia (who-4-temperature-control, ownkb:chunk:r000185): WHERE = Z#N (Z = 1..99, N = 1..9) addresses actuator N of zone Z; #4#IF routes across local bus interface IF; Dimension 22 split control uses 3#Z#N.
  • Firmware Oracle (thermo-zone-requests, thermo-actuator, trace-corpus):
    • *#4*0##: ack/out on F453, F453AV, MH200N; nack/silent on F450, F454, F459, F460, F461, H4684, MH200, MH202, MyHomeServer1.
    • *#4*1##: out ($0499013000, 3012, 300E, 300D) on F454 020051; out on F461 020011 and MyHomeServer1 028206; ack/out on MH202 010024; nack/out (byte-identical 4 SCS telegrams to F454) on F453, F453AV, MH200N (left None as unverified until live multi-session heating traces exist).
    • *#4*1*20##: out on F454, F461, MyHomeServer1; nack/silent on F453, F453AV, H4684, MH200, MH200N, MH202.
    • *#4*1*60##: out on F461, MyHomeServer1; nack/silent on F453, F453AV, F454, H4684, MH200, MH200N, MH202.
    • *#4*2#4#01...##: zero of twelve emulated firmwares emit $06EC/$06E4 interface-routed WHO 4 bus frames.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

@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

@gdluck

gdluck commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Yes, allow me few minutes.

@gdluck

gdluck commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review of e377470, as asked. Short version: the parser change is correct and I would merge it; one of the five profile flags is not backed by the oracle rows it cites and should be None on four models (details in 3).

1. Parsing, verified on master vs this branch

I parsed the same frames with OWNMessage.parse on origin/master (2.0.0b13) and on this head:

Frame master this PR
*#4*30#4*20*1## _actuator="4" (private), interface=None actuator="4", interface=None
*#4*0#4*20*0## _actuator="4" actuator="4"
*#4*30#4#01*0*0230## interface=None, where params ['4','01'] interface="01", no where params
*#4*30#4#01*20*1## _actuator="4" (wrong) actuator="1", interface="01"
*#4*30#4#4#01*20*1## _actuator="4", interface=None actuator="4", interface="01"
*#4*7#1#4#01*20*1## _actuator="1", interface=None actuator="1", interface="01"
*#4*#0#4#01*#14*0215*3## where params ['4','01'] interface="01", central #0
*#4*#0#4*#14*0215*3## where params ['4'] unchanged (4-zone central 4)
*#4*3#4#1*22*1*0215*1*0## where params ['4','1'] unchanged (dimension 22 excluded)
*#4*30#4*20## / *#4*2#4#01*20## (commands) no .actuator actuator="4" / actuator="1", interface="01"

Master already decodes actuator 4 correctly on the plain Z#4 form; the OpenWebNet-HA/MyHOME#863 misrouting lives in MyHOME's thermo_event_actuator fallback, which this PR bypasses by making .actuator public. With this head, MyHOME v2-phase1-architecture (without OpenWebNet-HA/MyHOME#864) already returns actuator 4 / interface 01 for all of the above; with #864 on top the results are identical. The dimension 22 exclusion is right: 3#Z#N with Z = 4 would otherwise match the ['4', IF] shape.

2. When the two halves actually occur

3. supports_heating_zone_status: the False values are not what the oracle says

The body states "*#4*1##: nack/silent on F453, F453AV, MH200N" and sets supports_heating_zone_status=False for F453, F453AV, F455 and MH200N. The committed rows say otherwise:

  • results/F453/010100/checks/trace-corpus.tsv, results/F453AV/030014/checks/trace-corpus.tsv, results/MH200N/010108/checks/trace-corpus.tsv: *#4*1## → nack, out, four SCS telegrams ($0499013000, $0499013012, $049901300E, $049901300D), byte-identical to the F454 row that the PR uses as evidence for True.
  • results/F455/010102/ has no WHO 4 rows at all.

Those four False values match MyHOME's conservative supports_zone_status list (born from the F455 single-session socket hangs in OpenWebNet-HA/MyHOME#674), which is a pacing policy, not firmware evidence. Under the "no behaviour change without traces" rule I would set F453, F453AV, F455 and MH200N to None (unverified) and correct the sentence in the body; MyHOME's own fallback table then keeps deciding, as OpenWebNet-HA/MyHOME#867 already handles the None case. If you prefer to keep False as a deliberate policy, the docstring and body should say so instead of citing nack/silent.

The other four flags match the committed rows (*#4*0## out on F453 / F453AV / MH200N only; *#4*1*20## and *#4*1*60## as listed; *#4*2#4#01*…## never bridged), and the MH202 promotion in e377470 matches *#4*1## → ack, out on MH202 010024 plus the live trace in OpenWebNet-HA/MyHOME#866.

I can push the None change and the body fix to this branch if you want it done here.

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

Copy link
Copy Markdown
Contributor Author

Thanks @gdluck! Great catch on the oracle rows for F453, F453AV, and MH200N in trace-corpus.tsv (emitting the 4 SCS telegrams identically to F454) and the F455 distinction.

I've pushed commit 124c32e which:

  1. Leaves supports_heating_zone_status as None (unverified) on F453, F453AV, F455, and MH200N, letting downstream pacing policy continue to decide.
  2. Updates test_who4_heating_capability_flags in tests/test_profiles.py accordingly.
  3. Corrects the PR description to reflect the oracle telegram rows accurately.

All 2,828 tests pass and linters / documentation sync checks are clean.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

@gdluck can you please rebase this one as well please

@gdluck

gdluck commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

The head branch of this PR is on your fork (GreenGrassBlueOcean/OWNd), where I have no push access, so I cannot rebase it myself. Here is the merge instead: git merge origin/master (c546a2e) onto 124c32e auto-merges CHANGELOG.md and base.py and conflicts only in OWNd/message/heating.py, OWNHeatingEvent.__init__. Resolution, as a diff against master's file (keeps the #156 wildcard / ASCII guard, adds this PR's interface split in front of it):

@@ -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 int(where) must not come back: it reintroduces the ValueError on *4*1**## that #156 removed. The other three heating.py hunks of this PR (.actuator on event and command, the #0#4#IF note) auto-merge.

Checked on the merged result (124c32e + c546a2e, this hunk, nothing else): ruff, mypy OWNd tests/typing, verify_library_standards.py, sync_documentation.py --check, update_readme_profiles.py --check, validate_corpus.py all clean; pytest -p pytest_cov --cov=OWNd 3361 passed, verify_coverage.py 100 % on all 28 OWNd modules (7126/7126); MyHOME v2-phase1-architecture f19adc57 suite with the merged tree on PYTHONPATH: 5601 passed, 1 deselected in 414.96s (the deselected one is the known test_check_all_documentation_real_tree pin signal). Parse probes on master, on 124c32e and on the merge: *#4*30#4*20*1## -> actuator 4; *#4*30#4#01*20*1## -> actuator 1, interface 01; *#4*30#4#4#01*20*1## -> actuator 4, interface 01; *#4*3#4#1*22*...## unchanged (dimension 22 excluded); *4*1**##, *4*303**## and a fullwidth zone *#4*1*0*0230## decode as zone 0 on master and on the merge, while 124c32e alone raises ValueError on the first two and reads zone 1 on the third.


Generated by Claude Code

@GreenGrassBlueOcean

GreenGrassBlueOcean commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

@gdluck can you please make a new pr than i will close this one

@gdluck

gdluck commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

New PR opened: #168 — your three commits (cc7a97b, e377470, 124c32e) rebased onto master 5cb9f2d, authorship kept; only the heating.py zone line needed the resolution already described above (master's _ascii_int guard kept, your where_params line added). Full check set and the MyHOME conformance run are green on the new head (details in the PR). This one can be closed.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants