Skip to content

feat(heating): OWNHeatingCommand.actuators_status(where) builder for *#4*WHERE*20## - #140

Draft
gdluck wants to merge 2 commits into
OpenWebNet-HA:masterfrom
gdluck:feat/who4-actuators-status-builder
Draft

gdluck wants to merge 2 commits into
OpenWebNet-HA:masterfrom
gdluck:feat/who4-actuators-status-builder

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A

Summary

OWNd has valves_status() for Dimension 19 (*#4*ZONE*19##) but no builder for the actuator status request *#4*WHERE*20##, although the gateways answer it per actuator. MyHOME's climate entity therefore starts with hvac_action: None and stays there until an actuator frame happens to arrive on the bus: its zone poll *#4*Z## does not include actuator status, and it has no way to request Dimension 20 (MyHOME climate.py initial None). On the live plant, sending *#4*2*20## by hand through myhome.send_message switched the entity to hvac_action: idle immediately.

Refs OpenWebNet-HA/MyHOME#792 (the consumer), OpenWebNet-HA/MyHOME#799 (the captures).

Changes

  • OWNd/message/heating.py: OWNHeatingCommand.actuators_status(where) builds *#4*<where>*20##. where may be a zone (2, #2), one actuator of a zone (2#1), a central-unit form (#0, #0#1) or #Z#N (an actuator addressed through the central unit). It must fully match #?[0-9]+(?:#[0-9]+)?(?:#4#[0-9]{2})? (ASCII digits only, fullmatch, whitespace stripped as the other builders do), so *, None, 2##, ##0, three-part forms other than the #4#NN routing suffix below and Unicode digits raise ValueError. No dimension can be injected through the address; the tests also assert the built frame never matches a WHO 4 Dimension 22 shape (the frame that crashes bt_termo).
  • The address grammar: Z#N (actuator N of zone Z) and #Z#N are the WHO 4 WHERE forms for actuators, which OWNd's _normalize_zone already documents ("4-zone central (#0#N) or probe under central (#Z#C / Z#C)") and OWNHeatingEvent already decodes (_actuator). Live evidence exists for 2, #0 (F461) and 35, #0 (MyHomeServer1) only; 2#1, #2, #0#1 and #2#1 are accepted on the strength of the spec and pinned by the tests so that a reviewer sees they were considered.
  • human_readable_log: "Requesting climate actuator status update for 2."
  • CHANGELOG.md: entry under [Unreleased] / Added.

Review follow-up (OpenWebNet-HA/MyHOME#792 review, finding 3). MyHOME sends the actuator request for a zone behind an F422 bus interface as *#4*3#4#01*20## (also #3#4#01, 2#1#4#01), and the first version of this builder rejected every three-part address. _WHO4_WHERE_REGEX now allows an optional #4#NN routing suffix (interface of exactly two digits) on the zone, the central-unit zone and the actuator form: Z#4#NN, #Z#4#NN, Z#N#4#NN. #0, #0#N, Z, Z#N and #Z are unchanged; *, an empty string, a one-digit (3#4#1) or three-digit (3#4#001) interface, a suffix other than #4#, a doubled suffix and a dimension after the suffix (3#4#01*22) stay refused. Evidence: own-firmware-oracle OpenWebNet-HA/own-firmware-oracle#60 (thermo-zone-requests) shows *#4*2#4#01*20## forwarded by F450, F454, F459, F460, F461, MH202 and MyHomeServer1 and dropped by F453, F453AV, H4684, MH200 and MH200N. The frame that crashes the MH202 thermoregulation daemon in that suite is the routed zone status without a dimension, *#4*2#4#01##; this builder only emits the dimension-20 form, so it never builds that frame. Docstring and CHANGELOG entry updated.

On the reply *#4*2#2*20*5## being pinned as is_active() is False. Dimension 20 values are 0 off, 1 on, 2 opened, 3 closed, 4 stop, 5 fan coil off, 6..8 fan speeds, 9 fan auto. OWNd master already decodes 5 as off: OWNHeatingEvent._actuator_active_states = ["1", "2", "6", "7", "8", "9"] and the _fan_mode branch (_fan_mode = value - 5; 0 -> self._fan_on = False, self._is_active = False). The test pins that unchanged behaviour; the live plant's fan coil was off.

State Diagram

stateDiagram-v2
    direction TB

    state "1. OWNHeatingCommand.actuators_status(where)" as Builder {
        [*] --> Validate
        Validate --> Built : fullmatch #?[0-9]+, #[0-9]+?, #4#[0-9]{2}?\n2 -> *#4*2*20##, #0 -> *#4*#0*20##, 2#1, #2, #0#1, #2#1, 3#4#01, #3#4#01, 2#1#4#01
        Validate --> Refused : , 2*22, #0*22, 2##, ##0, 2#1#4, a, Unicode digit, None\nValueError , before - no builder at all, AttributeError
    }

    state "2. Gateways answering the request (OpenWebNet-HA/MyHOME#799 captures)" as Gateway {
        [*] --> Idle
        Idle --> F461 : *#4*2*20##\ncommand session - *#4*2#1*20*0## then ACK , 111 ms\nevent session - *#4*2#2*20*5##
        Idle --> F461all : *#4*#0*20##\ncommand session - *#4*2#1*20*0##, *#4*2#2*20*5##, ACK , 2.1 s
        Idle --> MHS1 : *#4*35*20##\n*#4*35#1*20*0##, ACK , 189 ms
        Idle --> MHS1all : *#4*#0*20##\nACK , 0.39 s - 43 distinct actuator frames on the event session , 44 with the zone-35 reply
    }

    state "3. OWNHeatingEvent for a dimension 20 reply (unchanged)" as Reply {
        [*] --> Decode
        Decode --> Active : value 1, 2, 6, 7, 8, 9
        Decode --> Inactive : value 0 , off, 5 , fan coil off, 3, 4
    }
Loading

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 4 dimension 20, per zone and #0 trace PR: OpenWebNet-HA/MyHOME#799 (merged), f461_live_2026-10-23 run 1 (run1_oracle_replay_readonly: *#4*2*20##, *#4*#0*20##), run 2 (run2_reviewer_reads), run 6 and run 7 (the actuator frames after every *#4*#0*20## of the patched integration and after the manual *#4*2*20##) BTicino F461 2.0.11, standalone zone 2 (valve on actuator 1, fan coil on actuator 2) *#4*2*20## -> *#4*2#1*20*0## on the command session (ACK 111 ms), *#4*2#2*20*5## on the event session; *#4*#0*20## -> both on the command session (ACK 2.1 s). *#4*2#2*20*5## also seen as a spontaneous event.
WHO 4 dimension 20 on a 99-zone plant trace PR: OpenWebNet-HA/MyHOME#799 (merged), myhomeserver1_live_2026-10-24 run 2 (run2_reads_thermo_sound_alarm_lights) MyHomeServer1 3.87.13 (*#13**16*3*87*13##) *#4*35*20## -> *#4*35#1*20*0## (ACK 189 ms); *#4*#0*20## -> ACK 0.39 s with 43 distinct actuator frames (zones 0, 35..53, 55, 57..70; 44 counting the zone-35 reply), including the zone-0 actuators *#4*0#1*20*0## .. 0#4

What does the Oracle say

The published suites (oracle main 72759e8) contain no per-zone *#4*Z*20## request. They do contain the central-unit form *#4*#0*20## (results/<P>/<V>/oracle/full/thermo-actuator.tsv): nack / out on F450, F453, F453AV, F454, F459, F460, F461, MH200N and MyHomeServer1 (forwarded to the emulated bus as $04B9003010, $004B9003010 on F460 / F461, where no actuator answers) and nack / silent on H4684, MH200 and MH202, which refuse #0 locally. On the real plants *#4*#0*20## is forwarded and answered by the actuators (the F461 and MyHomeServer1 rows above: ACK with the dimension-20 frames), which is consistent with the nack / out rows of an emulated bus that has no actuator. The per-zone form is covered by the thermo-zone-requests suite merged in OpenWebNet-HA/own-firmware-oracle#60 (main 52d354b, 2026-10-25): findings/thermo-zone-requests.md records *#4*2*20##, *#4*2#1*20##, *#4*2#0*20## and *#4*2#4#01*20## per gateway as a firmware_observed verdict on emulated firmware (the TSV rows under results/<P>/<V>/oracle/full/ follow the next oracle CI run). This PR does not depend on those verdicts (a builder needs none); the two physical captures above are the evidence.

Testing

  • tests/test_who4_actuators_status.py (new, 23 cases): 8 addresses (2, 2 as int, #2, 2#1, #0, #0#1, #2#1, 22) build the expected frame, are is_dimension_request with dimension == 20, round-trip through OWNMessage.parse (same string, same where), and never match a Dimension 22 shape; 12 bad addresses raise ValueError; the three live replies (*#4*2#1*20*0##, *#4*2#2*20*5##, *#4*35#1*20*0##) parse as OWNHeatingEvent with the zone, the actuator 1 / 2 / 1, is_active() is False, fan_on None / False / None.
  • Fail before / pass after: on master 38 fail (AttributeError: ... has no attribute 'actuators_status'), 3 pass (the reply parsing already worked); on this branch all 41 pass. Re-verified 2026-10-24 with plain OWNd master as the sender: F461 *#4*2*20## ACK 95 ms with *#4*2#1*20*0## and *#4*2#2*20*5## (which of the two arrives on the command session varies between captures, so a consumer must collect both sessions); MyHomeServer1 3.87.13, status reads on all 33 zones: every *#4*Z*20## ACK in 155-227 ms, 28 zones answering actuator 1 only, zones 47 / 48 / 57 / 60 / 70 also *#4*Z#2*20*5##, zone 60 actuator 1 *#4*60#1*20*1## (on).
  • Full suite: 1930 passed, 1 skipped (master: 1907 passed, 1 skipped); 100 % statement coverage, mypy clean, ruff clean, scripts/sync_documentation.py --check and scripts/verify_library_standards.py pass.
  • Review follow-up tests (routed addresses): 5 accepted forms (3#4#01, " 3#4#01 ", #3#4#01, 2#1#4#01, 12#4#15) added to the build/round-trip case, 13 refused forms (3#4#1, 3#4#001, #3#4#1, #3#4#001, 2#1#4#1, 2#1#4#001, 3#4#, 3#4#0a, 3#4#01#, 3#3#01, 3#4#01#4#01, 3#4#01*22, *#4#01) added to the ValueError case; the module is now 41 cases. Fail before / pass after: on the previous tip of this branch 5 fail (ValueError: Invalid zone address), 36 pass; after the follow-up commit 41 pass. Full suite 1948 passed, 1 skipped, 100 % statement coverage (the 14 partial branches in connection/session.py and message/sound.py are unchanged from master), mypy OWNd tests/typing, ruff, scripts/sync_documentation.py --check and scripts/verify_library_standards.py pass.

Live retest (2026-10-24)

Confirmed for the standalone zone-2 path: with OpenWebNet-HA/MyHOME#792 on top of this builder, every start sent *#4*#0*20## and received *#4*2#1*20*0##, *#4*2#2*20*5##; hvac_action was idle immediately. The per-zone form *#4*2*20## was not sent in that run (it is in the 2026-10-23 capture above); no central unit and no other gateway model was tested.

Companion

MyHOME: OpenWebNet-HA/MyHOME#792 (request *#4*Z*20## once when a zone is added and after a reconnect, aggregating hvac_action over the zone's actuators).

🤖 Generated with Claude Code

@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!

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
…ension 20

OWNd had valves_status() for dimension 19 (`*michnovka#4*ZONE*19##`) but no
builder for the actuator status request `*michnovka#4*WHERE*20##`, although the
gateways answer it: on the live F461 2.0.11 (2026-10-23, MyHOME#799)
`*michnovka#4*2*20##` returned `*michnovka#4*2#1*20*0##` (valve, off) on the command
session and `*michnovka#4*2#2*20*5##` (fan coil, off) on the event session, one
frame per actuator, and `*michnovka#4*#0*20##` returned both; a live
MyHomeServer1 3.87.13 answered `*michnovka#4*35*20##` with `*michnovka#4*35#1*20*0##`.
MyHOME needs the request to fill hvac_action at startup instead of
waiting for a spontaneous actuator frame.

actuators_status(where) accepts a zone (`2`, `michnovka#2`), one actuator (`2#1`),
a central-unit form (`#0`, `#0#1`) or `#Z#N` (WHO 4 WHERE table; pinned
by a test): the address must fully match `#?[0-9]+(?:#[0-9]+)?`, so `*`,
whitespace, Unicode digits and anything else raise ValueError and no
dimension can be injected through it. Value 5 in a reply is fan coil
off (master `_actuator_active_states`), pinned unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the feat/who4-actuators-status-builder branch from 33cb2b7 to 7766f21 Compare October 24, 2026 15:49
…resses (review follow-up)

The MyHOME#792 review (finding 3) points out that MyHOME sends the
actuator request for zones behind an F422 bus interface as
`*michnovka#4*3#4#01*20##`, `*michnovka#4*michnovka#3#4#01*20##` or `*michnovka#4*2#1#4#01*20##`, while
`OWNHeatingCommand.actuators_status` rejected every three-part address
with `ValueError`. The `_WHO4_WHERE_REGEX` now allows an optional
`michnovka#4#NN` routing suffix (interface of exactly two digits) after the zone,
the central-unit zone or the actuator: `Z#4#NN`, `#Z#4#NN`, `Z#N#4#NN`.
`#0`, `#0#N`, `Z`, `Z#N` and `#Z` are unchanged; `*`, an empty string,
a one-digit (`3#4#1`) or three-digit (`3#4#001`) interface, a suffix
that is not `michnovka#4#`, a double suffix and a dimension smuggled after the
suffix stay refused.

Evidence: own-firmware-oracle#60 (`thermo-zone-requests`) shows
`*michnovka#4*2#4#01*20##` forwarded by F450, F454, F459, F460, F461, MH202 and
MyHomeServer1 and dropped by F453, F453AV, H4684, MH200 and MH200N. The
frame that crashes the MH202 thermoregulation daemon in that suite is
the routed zone status without a dimension, `*michnovka#4*2#4#01##`, which this
builder never emits: it only builds the dimension-20 form.

Tests: 5 accepted routed forms (fail before: `ValueError`) and 13
refused forms added to `tests/test_who4_actuators_status.py`; on the
previous tip 5 fail / 36 pass, now 41 pass. Full suite 1948 passed,
1 skipped, 100 % statement coverage; ruff, mypy (`OWNd tests/typing`),
`sync_documentation.py --check` and `verify_library_standards.py` pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
…d#140's builder requires (OpenWebNet-HA#792 review, 3)

`_ZONE_WHERE` accepted a one- or two-digit interface (`3#4#1`, `3#4#01`) while the
companion builder `OWNHeatingCommand.actuators_status` (OpenWebNet-HA/OWNd#140, 307dcc9)
requires exactly two (`michnovka#4#[0-9]{2}`) and rejects `3#4#1`, so swapping the string builder
for it would have broken a zone discovered from a frame that spells its interface with one
digit. The address is normalised before it is validated: `_validate_zone_where` returns the
address with a two-digit interface (`3#4#1` -> `3#4#01`), `request_zone_dimension` and
`watch_zone_actuators` store that form (one watch count per zone, whichever spelling asked),
and `_ZONE_WHERE` is tightened to `{2}`. In `climate.py`, `_calling_zones` and the entity's
`_full_where` spell the interface the way the bus frames and the configuration validator
(`BusInterface`, `zfill(2)`) do, so a routed zone has one address everywhere.

Tests: two fail before, pass after (`watch_zone_actuators("3#4#1")` counts `3#4#01` and
sends `*michnovka#4*3#4#01*20##`, unwatch removes it; `*michnovka#4*3#4#1*14*0215##` and `interface="1"`
both give `3#4#01`). `3#4#001` joins the refused addresses; `3#4#1` leaves the list of
addresses accepted as they are.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 25, 2026
…d#140's builder requires (OpenWebNet-HA#792 review, 3)

`_ZONE_WHERE` accepted a one- or two-digit interface (`3#4#1`, `3#4#01`) while the
companion builder `OWNHeatingCommand.actuators_status` (OpenWebNet-HA/OWNd#140, 307dcc9)
requires exactly two (`michnovka#4#[0-9]{2}`) and rejects `3#4#1`, so swapping the string builder
for it would have broken a zone discovered from a frame that spells its interface with one
digit. The address is normalised before it is validated: `_validate_zone_where` returns the
address with a two-digit interface (`3#4#1` -> `3#4#01`), `request_zone_dimension` and
`watch_zone_actuators` store that form (one watch count per zone, whichever spelling asked),
and `_ZONE_WHERE` is tightened to `{2}`. In `climate.py`, `_calling_zones` and the entity's
`_full_where` spell the interface the way the bus frames and the configuration validator
(`BusInterface`, `zfill(2)`) do, so a routed zone has one address everywhere.

Tests: two fail before, pass after (`watch_zone_actuators("3#4#1")` counts `3#4#01` and
sends `*michnovka#4*3#4#01*20##`, unwatch removes it; `*michnovka#4*3#4#1*14*0215##` and `interface="1"`
both give `3#4#01`). `3#4#001` joins the refused addresses; `3#4#1` leaves the list of
addresses accepted as they are.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants