Skip to content

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

Merged
GreenGrassBlueOcean merged 4 commits into
OpenWebNet-HA:masterfrom
gdluck:feat/who4-actuators-status-builder
Oct 26, 2026
Merged

GreenGrassBlueOcean merged 4 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]{1,2}(?:#[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), Z#0 and 0#0 are the WHO 4 WHERE forms for actuators in the Legrand document (openwebnet-who-4.pdf p. 22); #Z#N, #0#N and the bare Z / #0 forms are not in that document (they come from the catalogue, the oracle and the live gateways: see the PDF audit of 2026-10-26), which OWNd's _zone_address_and_name (OWNd/message/heating.py) 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. The suffix is accepted as spec-legal grammar only: OpenWebNet-HA/own-firmware-oracle#65 (thermo-zone-requests, decoded scs_bus_hex) shows that no emulated firmware routes *#4*2#4#01*20## across an F422. F450, F454, F459, F460, F461 and MyHomeServer1 truncate WHERE at the first # and poll local zone 2 ($04A9023010 / $004A9023010, the bytes of *#4*2*20##, also for *#4*2#4#12*20## and *#4*2#1#4#01*20##); MH202 010024 reads the trailing #4#01 as local zone 4 actuator 1 ($0499843110) and rejects #4#12; F453, F453AV, H4684, MH200 and MH200N drop all three routed forms (nack/silent). Callers must therefore gate #4#NN polling on the profile's supports_interface_zone_requests (OpenWebNet-HA/MyHOME#816). The MH202 010024 daemon crash (SIGSEGV, thermo_open2scs+0x394) hits the dimensionless status forms *#4*Z#N## (N 1..9) and *#4*Z#4#01..09##; this builder only emits the dimension-20 form, so it never builds them. Docstring, test docstring and CHANGELOG entry updated (review of 2026-10-26, commit d8bbbae); an earlier revision of this paragraph read the out verdicts of oracle #60 as "forwarded".

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 coil on (the PDF p. 22 says "ON Fan Coil"; OWNd reports it as the automatic speed). 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]{1,2}, #[0-9]?, #4#[0-9]{2}?\n2 -> *#4*2*20##, #0 -> *#4*#0*20##, 2#1, 2#0, 0#0, #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, 105, 2#10, True, -1\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

results/<P>/<V>/oracle/full/thermo-actuator.tsv carries the per-zone form *#4*1*20## on all 12 gateways (nack / out with $04A9013010 on F450, F454, F459, MyHomeServer1 and $004A9013010 on F460, F461; nack / silent on F453, F453AV, H4684, MH200, MH200N, MH202) and the central-unit form *#4*#0*20##: 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 zone, actuator and routed forms (*#4*2*20##, *#4*2#1*20##, *#4*2#0*20##, *#4*0#0*20##, *#4*2#4#01*20##, *#4*2#4#12*20##, *#4*2#1#4#01*20##) are on own-firmware-oracle main as thermo-zone-requests TSV rows (#60, #63, #65), and the closed-loop virtual plant rows in results/F461/020011/oracle/plant/plant-f461-bench.tsv (#64) answer both *#4*2*20## and *#4*#0*20## with the actuator frames. 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, 60 cases): 18 addresses (2, 2 as int, #2, 2#1, #0, #0#1, #2#1, 22, 2#0, 0#0, 0, 99, #0#4, and the routed 3#4#01, #3#4#01, 2#1#4#01, 12#4#15) build the expected frame, are is_dimension_request with dimension == 20, round-trip through OWNMessage.parse (same string, same where and where_param, the log line carries the address as written), and never match a Dimension 22 shape; 39 bad addresses raise ValueError (empty, *, dimension injection, Unicode digits, None, True, False, -1, three-digit probes 105 / 101 / 899 / 001 / 99999, two-digit actuators 2#10 / #0#10, one- or three-digit interfaces); 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 60 pass (review of 2026-10-26 14:57: regex bounded to zones 0..99 and one actuator digit, log line with the full address, where_param round trip; commit 0099a22). 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
@gdluck
gdluck force-pushed the feat/who4-actuators-status-builder branch from 33cb2b7 to 7766f21 Compare October 24, 2026 15:49
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>
gdluck and others added 2 commits October 26, 2026 05:49
…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>
…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>

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actuators_status builder and _WHO4_WHERE_REGEX grammar look clean — keeping the wire grammar permissive for spec-legal #4#NN forms makes sense at the OWNd message layer.

One important factual update for the docstrings and CHANGELOG.md from our follow-up investigation of the MH202 010024 crash in own-firmware-oracle#63 (own-firmware-oracle#65, OpenWebNet-Encyclopedia#52, openwebnet-mcp#22, MyHOME#816):

Inspecting the decoded scs_bus_hex bytes across all twelve emulated gateways shows that none of the seven bus_outcome=out gateways actually forward *#4*2#4#01*20## (or *#4*2#1#4#01*20##) across an F422 interface ($06EC... / $06E4...):

  • F450 020010, F454 020051, F459 020105, F460 020012, F461 020011, MyHomeServer1 028206 truncate WHERE at the first # and emit $04A9023010\r ($004A9023010\r on F460/F461) — the exact same bytes as unqualified *#4*2*20##, even for *#4*2#4#12*20## and *#4*2#1#4#01*20## — cross-polling local main-bus zone 2.
  • MH202 010024 (libthermo.so FUN_00018a04 + libplug_thermo_tr.so thermo_open2scs) takes the trailing two #-delimited integers ("4" and "01") of both *#4*2#4#01*20## and *#4*2#1#4#01*20## as local zone 4, actuator 1 and emits $0499843110\r on the local bus (*#4*2#4#12*20## is rejected with nack, silent because actuator 12 > 9).
  • For the same reason, MH202 010024 crashes coso (SIGSEGV at thermo_open2scs+0x394) not only on *#4*2#4#01## (01..09), but also on plain per-actuator status *#4*2#1## (OWNHeatingCommand.status("2#1"), Z=1..99, N=1..9).

Inline comments below point to the three prose spots so the documentation doesn't suggest those seven firmwares route #4#NN across an F422 interface.

Comment thread OWNd/message/heating.py Outdated
Comment on lines +878 to +883
``2#1#4#01``): 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In own-firmware-oracle#65 (and OpenWebNet-Encyclopedia#52), decoding scs_bus_hex on *#4*2#4#01*20##, *#4*2#4#12*20##, and *#4*2#1#4#01*20## shows that none of these seven images emit an interface-routed $06EC/$06E4 SCS telegram:

  • F450, F454, F459, F460, F461, and MyHomeServer1 truncate WHERE at # and emit $04A9023010\r ($004A9023010\r on F460/F461), querying local main-bus zone 2.
  • MH202 010024 misparses the trailing #4#01 tokens of 2#4#01*20 and 2#1#4#01*20 as local zone 4, actuator 1 ($0499843110\r) and rejects 2#4#12*20 (12 > 9, nack/silent).
  • Also worth noting that the MH202 010024 coso SIGSEGV (thermo_open2scs+0x394) affects both dimensionless per-actuator status *#4*Z#N## (N=1..9) and *#4*Z#4#01..09## (because Z#4#01 is misparsed as zone=4, actuator=1).

Keeping #4#NN accepted in _WHO4_WHERE_REGEX as spec-legal grammar is fine, as long as this docstring notes that callers must gate #4#NN polling on supports_interface_zone_requests (see MyHOME#816) rather than expecting those seven firmwares to route it across an F422.

Comment thread tests/test_who4_actuators_status.py Outdated
Comment on lines +15 to +20
sends them for zones behind an F422) are accepted since the MyHOME#792 review:
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
daemon there is ``*#4*2#4#01##`` (zone status, no dimension), which this
builder never emits: it only builds the dimension-20 form.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same clarification as in OWNd/message/heating.py: own-firmware-oracle#65 shows the seven out images emit unrouted local-bus queries ($04A9023010\r / $004A9023010\r for local zone 2, or $0499843110\r for local zone 4 actuator 1 on MH202 010024) rather than forwarding #4#NN across an F422, and MyHOME#816 withholds #4#NN WHO 4 status polls unless supports_interface_zone_requests is True.

Comment thread CHANGELOG.md Outdated
- Automatic detection of legacy pre-`09W29` BTicino `F414` coarse-only dimmers via `DIMENSION 1` / `DIMENSION 4` transition speed `240` (`0xF0`, `DIMMER_SPEED_COARSE_ONLY`) with multi-gateway plant isolation (`gateway=` scoping on `OWNMessage.parse`, `OWNEvent.parse`, `OWNEventSession.get_next`, `is_coarse_dimmer_where`, `register_coarse_dimmer_where`, `clear_coarse_dimmer_wheres`, and `OWNLightingCommand.set_brightness`) (`#134`, `MyHOME#767`).
- Canonical `libqtdevices` (`TS10_1_0_23`) coarse `WHAT 2..10` $\leftrightarrow$ fine `1..100%` conversion helpers (`COARSE_PRESET_TO_PERCENT`, `brightness_preset_to_percent`, `percent_to_brightness_preset`), `OWNLightingEvent.is_coarse_dimmer`, `OWNLightingEvent.brightness_preset_percent`, and `OWNLightingCommand.set_brightness(..., coarse=..., gateway=...)` (`#134`).
- `WHAT 39` end of movement (`*1*39*WHERE##`) from WHO 1 motion detectors: `message_type` `motion_detected` with `motion` `False`, new `OWNLightingEvent.motion_end`, `is_sensor` `True`, `is_on` `None`. It was `unknown_state` `39` before (`MyHOME#777`).
- **WHO 4 (`OWNHeatingCommand.actuators_status`)**: Actuator status request `*#4*WHERE*20##` for a zone, one actuator (`2#1`) or `#0` (all zones); a live F461 2.0.11 answers it per actuator (`*#4*2#1*20*0##`, `*#4*2#2*20*5##`). The address may be routed through a bus interface (`3#4#01`, `#3#4#01`, `2#1#4#01`; two-digit interface): own-firmware-oracle#60 shows `*#4*2#4#01*20##` forwarded by F450, F454, F459, F460, F461, MH202 and MyHomeServer1 (dropped by F453, F453AV, H4684, MH200, MH200N); the MH202 crash in that suite is the routed zone status without a dimension (`*#4*2#4#01##`), which the builder never emits (`MyHOME#792` review).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note here: suggest updating "forwarded by F450, F454, F459, F460, F461, MH202 and MyHomeServer1" (and noting *#4*2#1## alongside *#4*2#4#01## for the MH202 010024 crash) to match own-firmware-oracle#65.

@gdluck
gdluck force-pushed the feat/who4-actuators-status-builder branch from 307dcc9 to e3b600d Compare October 26, 2026 08:56
@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on master 87cafc3 (after #136): the CHANGELOG [Unreleased] entry and the heating.py regex block now sit beside the merged dimension-22 guard; content unchanged, suite and 100 % coverage green.

…#65 (review follow-up)

The `actuators_status` docstring, the test module docstring and the
CHANGELOG entry said own-firmware-oracle#60 showed `*michnovka#4*2#4#01*20##`
"forwarded by F450, F454, F459, F460, F461, MH202 and MyHomeServer1".
own-firmware-oracle#65 decodes the SCS bytes of those `out` verdicts: the
six Linux gateways truncate WHERE at the first `#` and poll local zone 2
(the bytes of `*michnovka#4*2*20##`), MH202 010024 reads the trailing `michnovka#4#01` as
local zone 4 actuator 1 and rejects `michnovka#4#12`, and F453, F453AV, H4684,
MH200 and MH200N drop all three routed forms. Nothing routes `michnovka#4#NN`
across an F422, so the text now says callers must gate it on the profile's
`supports_interface_zone_requests` (MyHOME#816), and the MH202 crash note
covers the plain per-actuator status `*michnovka#4*Z#N##` as well as
`*michnovka#4*Z#4#01..09##`. Grammar and behaviour unchanged.

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

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, applied in d8bbbae (prose only, grammar and behaviour unchanged): the actuators_status docstring, the test module docstring and the CHANGELOG entry now follow OpenWebNet-HA/own-firmware-oracle#65 instead of reading the out verdicts of #60 as "forwarded".

  • F450, F454, F459, F460, F461 and MyHomeServer1 truncate WHERE at the first # and poll local zone 2 ($04A9023010 / $004A9023010) for *#4*2#4#01*20##, *#4*2#4#12*20## and *#4*2#1#4#01*20##; MH202 010024 reads the trailing #4#01 as local zone 4 actuator 1 ($0499843110) and rejects #4#12; F453, F453AV, H4684, MH200 and MH200N drop all three (nack/silent in the chore(release): bump version to 2.0.0-b9 #65 TSVs).
  • Both docstrings and the CHANGELOG now say callers must gate #4#NN polling on supports_interface_zone_requests (fix(climate): gate unrouted WHO 4 #4#II and actuator status polls MyHOME#816) rather than expecting an F422 route.
  • The MH202 010024 crash note now covers *#4*Z#N## (N 1..9) next to *#4*Z#4#01..09##, with the thermo_open2scs+0x394 location.

The PR body paragraph with the old sentence is corrected the same way. tests/test_who4_actuators_status.py (41 passed) and ruff are unchanged.

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update in d8bbbaed — the #4#NN non-routing findings and the MH202 010024 coso SIGSEGV boundary (*#4*Z#N## and *#4*Z#4#01..09## vs dimension-20 *#4*...*20##) now match OpenWebNet-HA/own-firmware-oracle#65 (thermo-zone-requests).

I checked the live traces (f461_live_2026-10-23 and myhomeserver1_live_2026-10-24), the oracle TSVs, the test cases, and the input validation. A few findings before this comes out of draft:

1. Proof & Documentation (_normalize_zone & Oracle section)

  • Non-existent _normalize_zone reference:
    • OWNd/message/heating.py:911, tests/test_who4_actuators_status.py:51, and the PR body (## Changes, bullet 2) all cite _normalize_zone (see _normalize_zone / _normalize_zone documents #Z#C).
    • There is no _normalize_zone in OWNd (that's in MyHOME's climate.py). The helper in OWNd/message/heating.py:164 that carries the # 4-zone central (#0#N) or probe under central (#Z#C / Z#C) comment is _zone_address_and_name.
  • PR body ## What does the Oracle say is slightly stale:
    1. It states "The published suites (oracle main 72759e8) contain no per-zone *#4*Z*20## request." In fact, results/<P>/<V>/oracle/full/thermo-actuator.tsv already contains *#4*1*20## across all 12 gateways (nack/out with $04A9013010 on F450, F454, F459, MyHomeServer1 and $004A9013010 on F460, F461; nack/silent on F453, F453AV, H4684, MH200, MH200N, MH202).
    2. The results/<P>/<V>/oracle/full/thermo-zone-requests.tsv rows from own-firmware-oracle#65 and the closed-loop virtual plant rows in results/F461/020011/oracle/plant/plant-f461-bench.tsv (own-firmware-oracle#64, answering both *#4*2*20## and *#4*#0*20##) are now committed on own-firmware-oracle main.

2. _human_readable_log truncates compound WHERE and uses dead _interface_log_text (OWNd/message/heating.py:942)

message = cls(f"*#4*{where_str}*20##")
message._human_readable_log = (
    f"Requesting climate actuator status update for {message._where}{message._interface_log_text}."
)

Two issues here:

  1. OWNMessage.interface (OWNd/message/base.py:301) only checks self._who in [0, 1, 2, 14, 15], so message._interface_log_text is always "" for WHO = 4 (and for 2#1#4#01, self._where_param is ["1", "4", "01"] where _where_param[0] != "4" anyway).
  2. Because OWNMessage._DIMENSION_REQUEST splits WHERE at the first # into self._where + self._where_param, message._where is "2" for "2#1" and "2#1#4#01", "#0" for "#0#1", and "3" for "3#4#01". As a result, actuators_status("2"), actuators_status("2#1"), and actuators_status("2#1#4#01") all log the exact same string:
    "Requesting climate actuator status update for 2."
    Using {where_str} (and dropping the dead {message._interface_log_text}) preserves the full target in the log message.

3. Input feasibility in _WHO4_WHERE_REGEX (OWNd/message/heating.py:55)

Currently:

_WHO4_WHERE_REGEX = re.compile(r"#?[0-9]+(?:#[0-9]+)?(?:#4#[0-9]{2})?", re.ASCII)

Because both the zone token and the #N actuator token use unbounded [0-9]+:

  • 3-digit secondary probes (105, 101..899) and arbitrary numbers (99999) are accepted: In WHO 4, zones are 1–2 digits (0..99, where 1..99 are zones and 0 / #0 is general/central). 3-digit addresses (PZZ = 101..899) are secondary temperature probes (which have no actuators, NACK after 2.1 s on F461, and are decoded by OWNHeatingEvent as sensor = P, zone = ZZ). Compare get_fan_speed / set_fan_speed in the same class, which bound the zone to 1..99.
  • Multi-digit actuator indices (2#10, 2#999) are accepted: Per the WHO 4 spec (ownkb:claim:c003280) and thermo-zone-requests.md, the actuator index N in Z#N / #Z#N / #0#N is a single digit 0..9 (1..9 for actuator N, 0 for all actuators of the zone as in 2#0 / 0#0).
  • Bounding the regex to r"#?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})?" rejects 3-digit secondary probes ("105"), "001", "99999", and multi-digit actuator numbers ("2#10") while keeping every valid zone (0..99), #0, Z#0, 0#0, Z#1..Z#9, #0#1..#0#4, #Z#1..#Z#9, and #4#NN form.

4. Test cases (tests/test_who4_actuators_status.py)

  • In test_actuators_status_builds_dimension_20, assert parsed.where == command.where only compares the first token before # (e.g. "2" for "2#1" and "2#1#4#01"). Please also assert assert parsed.where_param == command.where_param (and command.human_readable_log) on the round-trip.
  • Worth adding to the parametrized tables:
    • Accepted: "2#0" and "0#0" (the Encyclopedia Z#0 / 0#0 all-actuators forms, which are also the forms F453, F453AV, and MH200N actually forward onto the SCS bus per thermo-zone-requests.md).
    • Rejected: "105" (secondary probe), "2#10" (2-digit actuator), True / False (bool is a subclass of int in Python), and -1.

…eview follow-up)

Review of 2026-10-26 14:57 on OpenWebNet-HA#140:

- `_WHO4_WHERE_REGEX` is bounded to `#?[0-9]{1,2}(?:#[0-9])?(?:michnovka#4#[0-9]{2})?`:
  zones are 0..99, the actuator index is one digit (`2#0` / `0#0` for all
  actuators), the interface stays two digits. Three-digit secondary probes
  (`105`, `101..899`), `001`, `99999` and multi-digit actuators (`2#10`) now
  raise `ValueError`, like `get_fan_speed` bounds its zone.
- The log line uses the address as written (`2#1#4#01`) instead of
  `message._where` (which keeps only the zone) and the `_interface_log_text`
  that is always empty for WHO 4.
- Docstrings point at `_zone_address_and_name` (there is no `_normalize_zone`
  in OWNd).
- Tests: the round trip also asserts `where_param` and `human_readable_log`;
  accepted `2#0`, `0#0`, `0`, `99`, `#0#4`; refused `105`, `101`, `899`,
  `001`, `99999`, `OpenWebNet-HA#105`, `105#4#01`, `2#10`, `2#999`, `#0#10`, `2#10#4#01`,
  `True`, `False`, `-1`.

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

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass. All four points are in 0099a22:

  1. _normalize_zone does not exist in OWNd; the builder docstring, the test comment and the PR body now point at _zone_address_and_name (OWNd/message/heating.py). The "What does the Oracle say" section is rewritten: thermo-actuator.tsv does carry *#4*1*20## on all 12 gateways (nack/out with $04A9013010 / $004A9013010 on the six forwarding images, nack/silent on the rest), and the thermo-zone-requests rows (feat(heating): parse WHO 4 dimension 7 zone state (#58) #60, fix(profiles): automate README profiles table and derive features from profile fields #63, chore(release): bump version to 2.0.0-b9 #65) and the plant-f461-bench.tsv closed-loop rows (docs: relicense the project under Apache-2.0 (contributor consent needed) #64) are on main.
  2. Log line: message._interface_log_text is always empty for WHO 4 (OWNMessage.interface only looks at WHO 0/1/2/14/15) and message._where keeps only the token before the first #, so 2, 2#1 and 2#1#4#01 logged the same string. The builder now logs the address as written: Requesting climate actuator status update for 2#1#4#01.
  3. Regex bounded to #?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})? with re.ASCII: zones 0..99, one actuator digit (2#0 / 0#0 for all actuators), two-digit interface. 105, 101, 899, 001, 99999, #105, 105#4#01, 2#10, 2#999, #0#10, 2#10#4#01 now raise ValueError; every form you listed as valid still builds.
  4. Tests: the round trip asserts where_param and human_readable_log as well as where; accepted rows added for 2#0, 0#0, 0, 99, #0#4; refused rows for 105, 2#10, True, False, -1 (and the others above). 60 cases.

Checks on this head: OWNd suite 2099 passed, 1 skipped; ruff clean; mypy clean on heating.py; sync_documentation.py --check ok; the MyHOME v2-phase1-architecture head (f4a5a907) suite against this OWNd head 4991 passed, 1 skipped. _WHO4_WHERE_REGEX has no other caller, so nothing else changes behaviour.

@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review October 26, 2026 15:32

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all four points are cleanly addressed in 0099a22 (_WHO4_WHERE_REGEX bounded to #?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})?, full where_str logged without the dead _interface_log_text, _zone_address_and_name & oracle references updated, and 60 test cases covering where_param, human_readable_log, 2#0/0#0, secondary probes, multi-digit actuators, and bool/negative inputs).

One small housekeeping item on the PR description before merge:

  • Could you update the two remaining occurrences of #?[0-9]+(?:#[0-9]+)?(?:#4#[0-9]{2})? in the PR body (## Changes bullet 1 and the Validate --> Built / Validate --> Refused transitions in ## State Diagram) to match the bounded regex #?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})??

LGTM!

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 247f86e into OpenWebNet-HA:master Oct 26, 2026
8 checks passed
@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the merge. The PR description is updated: the ## Changes bullet and both Validate transitions in the state diagram now show the bounded regex #?[0-9]{1,2}(?:#[0-9])?(?:#4#[0-9]{2})? (the diagram also lists 2#0 / 0#0 as built and 105, 2#10, True, -1 as refused).

gdluck pushed a commit to gdluck/OWNd that referenced this pull request Oct 26, 2026
…dress

The guard refused `#Z#C` and its docstring said it is not a central-unit
form, but `set_mode("23#1")` in the same file builds exactly
`*4*311*OpenWebNet-HA#23#1##`, the frame libqtdevices sends, and the firmware oracle
`thermo-ts10` suite shows all 12 emulated gateways forward it. The regex
was also unbounded (`#999`, `#0#99`, `michnovka#1` built frames) while the sibling
`_WHO4_WHERE_REGEX` (OpenWebNet-HA#140) is bounded, and it did not strip whitespace
while `actuators_status` does.

`_WHO4_CENTRAL_WHERE_REGEX` is now `#(?:0(?:#[1-4])?|[1-9][0-9]?(?:#[1-9])?)`:
`#0`, `#0#1..#0#4`, `michnovka#1..OpenWebNet-HA#99`, `#Z#1..#Z#9`; `where` is stripped before
the match and quoted in the error. Tests: `OpenWebNet-HA#23#1`, `michnovka#1#2`, `#0#4`, `OpenWebNet-HA#99`,
padded `#0` accepted with their log line; `michnovka#1`, `OpenWebNet-HA#100`, `#999`, `#0#0`,
`#0#5`, `#0#99`, `michnovka#1#0`, `michnovka#1#10`, `michnovka#1#2#3`, `None`, `True`, `-1` refused;
`set_mode("23#1")` and `set_central_mode("OpenWebNet-HA#23#1")` build one frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/OWNd that referenced this pull request Oct 26, 2026
Drops the `_WHO4_WHERE_REGEX` hunk and its tests: `#0#C` on a 4-zone
central unit is the unit's configured zone selector, 1..99 (01..99), not
a 1..4 slot (MyHOME_Suite OPEN.db `#0#[ZA][ZB]`, MHCatalogue 4695
`ZAZB_CENTRAL` 1..99, oracle thermo-central-mode `*4*0*#0#2##` ->
`D1 02 ...`), so bounding it to `[1-4]` rejected valid addresses. The
merged OpenWebNet-HA#140 regex stays as it is. The CHANGELOG keeps the
`send_to_gateway` correction, now with the subject list reading
"... `AsyncSerialTransport.send` refuse it".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/OWNd that referenced this pull request Oct 26, 2026
… 0 WHERE, fixtures cited (review follow-up)

Review of 2026-10-26 20:32 on OpenWebNet-HA#145:

- DIMENSION 7 (`BITMASK_DIA_A`, OPEN.db) carries the same 24-bit mask as
  DIMENSION 11: MyHOME's device_health pairs (7, 11) and the MyHomeServer1
  cover diagnostic in MyHOME issue_466 sends `*#1001*0*7*1111…0111##` with
  the same bits as the live DIMENSION 11 reads. The decoder accepts both.
- WHO 1001 joins the WHOs whose WHERE may carry `michnovka#4#II` in
  `OWNMessage.interface`, so `interface`, `unique_id` (`1001-74#4#01`)
  and the log line cover an actuator behind an F422, as for WHO 1 / 2.
- The log subject handles an empty WHERE (the gateway's own diagnostic)
  without a double space; `cleared_bits` is stored once in `__init__`.
- Docstring, README catalog row and CHANGELOG: WHO 1001 is the diagnostic
  WHO of lighting and automation actuators alike; the mask is pushed after
  `*1*19*WHERE##` (oracle what19; MyHOME fixtures issue_619 F453AV,
  issue_466 / issue_733 MH200), pushed alongside normal `*1*0/1*WHERE##`
  sweep replies on an MH200N (issue_434, issue_746), answered on a direct
  read (live F461 / MyHomeServer1, 2026-10-24) and sent as DIMENSION 7 on
  WHERE 0 (issue_466).
- Tests: WHERE `0`, `0014`, routed `74#4#01`, empty, the status-shaped
  `*1001*4*0##`, DIMENSION 7 shapes, `is_general False` pinned; the
  firmware-oracle conformance block asserts the class, mask and cleared
  bits.
- Merged origin/master (050dc1d) into the branch (CHANGELOG conflict with
  OpenWebNet-HA#136 / OpenWebNet-HA#140 / OpenWebNet-HA#148 resolved, the WHO 1001 entry under "Added").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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