Skip to content

fix(heating): set_central_mode requires a central-unit address - #137

Draft
gdluck wants to merge 4 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/who4-set-central-mode-address
Draft

gdluck wants to merge 4 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/who4-set-central-mode-address

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A

Summary

OWNHeatingCommand.set_central_mode("1", CLIMATE_MODE_AUTO) built *4*311*1##: the builder accepted any where. The central-unit WHATs (311 automatic, 1 heating, 303 off, ...) address the central unit by API contract: #77 fix 3 ("*4*311*1## produces no bus frame; *4*311*#1## does") and the addressing of the central-unit commands: the Legrand WHO 4 document (openwebnet-who-4.pdf, WHERE table p. 6, commands pp. 8-12 and 24-27) has #0 (99-zone central unit) and #1..#99 (a zone set up by the central unit); the 4-zone forms #0#C and #Z#C are not in that document and come from MyHOME_Suite OPEN.db / MHCatalogue 4695 and the firmware oracle (thermo-central-mode, thermo-ts10). The mode codes themselves are the ones BTicino's own client sends (libqtdevices): the PDF lists 303 and 102 on #0 (pp. 24, 27) and 311 / 302 on #Z only (pp. 9, 12), and has no *4*1*#0## / *4*0*#0## command, so this builder follows the client, not the document, for 1 / 0 / 311 / 302 on #0. The zone-level builder for a plain zone is set_mode(where, ...).

The guard therefore applies to every WHAT this builder produces, by contract. The oracle evidence per WHAT is uneven and is listed honestly below: the plain form of 311 is nack/silent on all 12 emulated gateways and NACKed by a live F461 2.0.11, while the plain forms of 1 and 303 are forwarded to the bus by some gateways (as a frame addressed to zone 1, which is not what the central-unit builder is for).

Refs #77. The central_status deprecation-text correction that this PR carried at first is now its own PR (see Companion).

Changes

  • OWNd/message/heating.py: _WHO4_CENTRAL_WHERE_REGEX = #(?:0(?:#(?:0[1-9]|[1-9][0-9]?))?|(?:0[1-9]|[1-9][0-9]?)(?:#(?:0[1-9]|[1-9][0-9]?))?); set_central_mode strips where and requires it to fullmatch: #0; #0#C with C the 4-zone unit's configured zone 1..99 or 01..99 (MyHOME_Suite OPEN.db #0#[ZA][ZB], MHCatalogue 4695 ZAZB_CENTRAL, oracle thermo-central-mode *4*0*#0#2## → D1 02 …); #Z with Z 1..99 or 01..99; #Z#C with central unit C 1..99 (libqtdevices' *4*311*#23#1##, the frame set_mode("23#1") builds). ASCII digits, so #١ is refused; #00, #0#0, #0#00 and three-digit values (#999, #0#100) are refused. Revisions: e8e20fb stopped refusing #Z#C and bounded the digits (adversarial scan; the firmware oracle thermo-ts10 suite shows all 12 gateways forward *4*311*#23#1##); 406d155 widened the selectors from 1..4 / 1..9 to 1..99 after the docs(changelog): describe send_to_gateway's dimension 22 refusal as it is #148 review (the 1..4 reading of #0#N was wrong). Anything else raises ValueError("Central unit mode needs a central unit address such as '#0': ..."). The default #0 is unchanged. Callers checked: MyHOME climate.py passes self._where, which is #0 or #0#1 when _central; the golden corpus uses #0 and #0#1.
  • CHANGELOG.md: entry under [Unreleased] / Fixed.

State Diagram

stateDiagram-v2
    direction TB

    state "1. OWNHeatingCommand.set_central_mode(where, mode)" as Builder {
        [*] --> Validate
        Validate --> Built : where fullmatch #0 | #0#C | #Z | #Z#C, C and Z 1..99 or 01..99\n*4*311*#0##, *4*1*#0#1##, *4*311*#0#12##, *4*303*#23##, *4*311*#23#1##
        Validate --> Refused : plain zone 1 / 2 / 0, 23#1, #, ##0, #0*22, #a, #١, #00, #0#0, #999, #0#100\nValueError , before - *4*311*1## was built and sent
    }

    state "2. What the gateways do with the two forms of 311 (oracle thermo-zone-mode, ownd-pr82-heating; live F461 2.0.11)" as Gateway {
        [*] --> Plain
        Plain --> NackSilent : *4*311*1## nack/silent on 12 of 12\nlive *4*311*2## NACK
        [*] --> Central
        Central --> Forwarded : *4*311*#1## ack/out on 9, nack/out on F460 / F461 / MyHomeServer1\nlive *4*311*#2## ACK , no central unit - changes nothing
    }
Loading

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 4 central-unit mode, plain vs #Z form upstream MyHOME fixture tests/fixtures/traces/f461_live_2026-10-15 (merged), runs run3_climate and probe_followup BTicino F461 2.0.11, standalone zone 2, no central unit *4*311*2## NACKed; *4*311*#2##, *4*303*#2##, *4*302*#2##, *4*102*#2##, *4*202*#2## ACKed (they change nothing on a plant without a central unit)
WHO 4 central-unit reads on plants without a central unit trace PR: OpenWebNet-HA/MyHOME#799 (open), f461_live_2026-10-23 run 1 (run1_oracle_replay_readonly) and myhomeserver1_live_2026-10-24 run 2 (run2_reads_thermo_sound_alarm_lights) F461 2.0.11; MyHomeServer1 3.87.13 (*#13**16*3*87*13##) F461: *#4*#2##, *#4*#2*14##, *#4*#0##, *#4*#0*14## NACK after the bus timeout. MyHomeServer1: *#4*#0## and *#4*#0*14## NACK after ~4.2 s. No *4*311*...## was sent on the MyHomeServer1 (a real home).

What does the Oracle say

Suites thermo-zone-mode and ownd-pr82-heating (results/<P>/<V>/oracle/full/<suite>.tsv; the rows are identical in both where both carry the frame). Twelve gateways: F450 020010, F453 010100, F453AV 030014, F454 020051, F459 020105, F460 020012, F461 020011, H4684 020054, MH200 020102, MH200N 010108, MH202 010024, MyHomeServer1 028206.

Frame ack / out nack / out (forwarded, no central unit on the emulated bus) nack / silent
*4*311*1## (plain, this builder before the fix) none none all 12
*4*311*#1## (central form, this builder) F450, F453, F453AV, F454, F459, H4684, MH200, MH200N, MH202 (9) F460, F461, MyHomeServer1 none
*4*1*1## (plain heating) F450, F453, F453AV, F454, F459, MH200N, MH202 (7) F460, F461, MyHomeServer1 H4684, MH200
*4*0*1## (plain conditioning) the same 7 F460, F461, MyHomeServer1 H4684, MH200
*4*303*1## (plain off) F450, F454, F459, MH202 (4) F460, F461, MyHomeServer1 F453, F453AV, H4684, MH200, MH200N

So "every gateway refuses the plain form" holds for 311 only. For 1, 0 and 303 the plain form is a zone command that some gateways forward; this builder refuses it anyway because it is the central-unit builder (contract above), and set_mode(where, ...) remains the zone-level builder. No published row exists for the plain forms of 302, 102 and 202 (the live F461 ACKs their #2 forms, table above).

Testing

  • tests/test_who4_central_mode_address.py (new, 49 cases): test_set_central_mode_requires_a_central_address (28: 1, 2, 0, 23#1, empty, blank, #, ##0, #0*22, #a, #0\n1, an Arabic-Indic digit, #00#1, #0#, #00, #100, #999, #0#0, #0#00, #0#100, #0#999, #1#0, #1#00, #1#100, #1#2#3, None, True, -1), test_set_central_mode_refuses_a_plain_zone_for_every_mode (3: 311, 1, 303 on where="1"), test_set_central_mode_keeps_central_addresses (16, frame and log line: #0, #0#1, #0#4, #0#5, #0#05, #0#12, #0#99, #1, #01, #23, #99, #23#1, #23#12, #1#2, padded #0, #0 with a trailing newline), test_set_central_mode_agrees_with_set_mode_on_probe_addresses (set_mode("23#1") == set_central_mode("#23#1")), test_set_central_mode_default_is_the_central_unit.
  • Fail before / pass after: on master 17 fail, 4 pass (set_central_mode('1', 'auto') returns *4*311*1##, #1#2 is accepted); on this branch all 49 pass.
  • Full suite (on master 6c3930d after fix(connection): refuse WHO 4 dimension 22 in every send path #136 / fix(profiles): alias H/L4684 & HC4684 to H4684Profile and enable WHO 22 on FW >= 6.0.0 #147, head 406d155): 2088 passed, 1 skipped; 100 % statement coverage (scripts/verify_coverage.py), mypy OWNd tests/typing clean, ruff check clean, scripts/sync_documentation.py --check and scripts/verify_library_standards.py pass.

Live retest (2026-10-24)

Not exercised live: neither plant has a central unit and *4*311*...## was not sent (the plant's zone 2 is standalone; set_central_mode('2', 'auto') now raises without a gateway).

Companion

MyHOME: none needed (MyHOME already passes a central-unit address). Related OWNd PR: docs(heating): central_status deprecation text names the real reason (split out of this PR): #143.

🤖 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 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@GreenGrassBlueOcean all the PR's where tripple checked with oracle, test plant - tested with fable and co-verified with kimi3-pro. If anything, let me know

@GreenGrassBlueOcean

GreenGrassBlueOcean commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @gdluck ,can you please split out the trace pr so that we can add the actual physical trace to the myhome repo? Like we did with all traces before?

@gdluck

gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Done: the physical capture is split out as OpenWebNet-HA/MyHOME#799 (tests/fixtures/traces/f461_live_2026-10-23/, derived_trace_archive JSON + README + 21 replay tests, synthetic MAC/IP, green on v2-phase1-architecture with plain OWNd master). It carries the 2026-10-23 F461 2.0.11 rows behind #136–#142 and OpenWebNet-HA/MyHOME#791–#798; the *4*311*2## NACK / *4*311*#2## ACK pair cited here is already upstream in f461_live_2026-10-15, so #799 references it instead of duplicating it.

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
…penWebNet-HA#136-OpenWebNet-HA#142 and MyHOME OpenWebNet-HA#791-OpenWebNet-HA#798

The maintainer asked on OpenWebNet-HA/OWNd#137 (comment 6384484511) for the
physical trace behind the 2026-10-24 pull requests to be added to this repo
like every capture before. Same plant and format as f461_live_2026-10-15 and
f461_live_2026-10-18: a BTicino F461 2.0.11 (WHO 13 device type 200, WHO 1013
134.15.5.0) with BT-F411/4 lights at 06 / 07, an F422 light at 09 whose
actuator lacked configurator 2 at capture time, a BT-F411/2 5 s shutter at 05
and a KW4691 standalone thermostat at zone 2 with a BT-F430/2 valve and a
BT-F430/4 fan coil; no central unit.

tests/fixtures/traces/f461_live_2026-10-23/: the derived trace archive (299
event-session frames, 939 tx / rx / result rows in ten runs: the oracle replay
of 165 read-only requests, reviewer reads, setpoint restore, final state,
report-form writes, the event session during a local Home Assistant run, the
bus monitor of an earlier run, two flicker re-tries) and its README with the
plant, the runs, what the plant established and which pull request each frame
backs. MAC / IP are synthetic in both their text and WHO 13 byte forms.

tests/test_f461_live_2026_10_23_trace.py pins the facts the pull requests rely
on: the *michnovka#4*2## reply order with dimension 12 before and different from 14,
*michnovka#4*2*20## followed by two actuator frames and *michnovka#4*#0*20## answering both,
*#1*0007## NACK vs *#1*07## ACK vs *#1*7## empty, *OpenWebNet-HA#13**0## echoed on the
event session, every WHO 16 request refused in under 10 ms, the dimension 27
model name frame, *michnovka#4*#2*14## and the other central-unit forms refused, one
*2*0*0##, the shutter's own stop 5.1-5.9 s after each run; plus entity replays
through handle_event that pass on the base branch with the pinned OWNd.

tests/test_trace_oracle_validation.py: tier 4 tolerates the authentic
*OpenWebNet-HA#13**27*70#52#54#49## reply until the pinned OWNd carries the dimension 27
decoder (OWNd#139); it parses to None without raising. The *4*311*2## NACK /
*4*311*#2## ACK pair cited by OWNd#137 is already in f461_live_2026-10-15 and
is referenced, not duplicated. scripts/update_trace_matrix.py leaves the
matrix unchanged (the F461 row already covers WHO 1, 2, 4, 13, 1013).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/who4-set-central-mode-address branch from 844f4fb to a6c4cdc Compare October 24, 2026 15:44
@gdluck gdluck changed the title fix(heating): set_central_mode requires a central-unit address; correct the central_status deprecation text fix(heating): set_central_mode requires a central-unit address Oct 24, 2026
Central-unit WHATs (311, 1, 303, ...) address the central unit by API
contract (OWNd#77 fix 3): `where` must be `#0`, `#0#N` or `#Z`; `#Z#N`
and plain zones are refused with ValueError. The plain zone form
`*4*311*Z##` is nack/silent on all 12 emulated gateways (own-firmware-
oracle thermo-zone-mode, ownd-pr82-heating) and a live F461 2.0.11 NACKs
`*4*311*2##` while it ACKs `*4*311*michnovka#2##`. The default `#0` is unchanged.

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

Copy link
Copy Markdown
Contributor

@gdluck please rebase

@gdluck
gdluck force-pushed the fix/who4-set-central-mode-address branch from a6c4cdc to 0b29827 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.

…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

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Self-correction in e8e20fb (no review asked for it). @GreenGrassBlueOcean could you confirm the reading below, or say if you see it differently?

Context. This PR made set_central_mode refuse plain zone addresses because *4*311*Z## is nack/silent on all 12 emulated gateways and on a live F461. The first revision also refused #Z#N and its docstring said "#Z#N is not a central-unit form".

How it was found. An adversarial read of the PR against its neighbours in OWNd/message/heating.py, not against its own claims: set_mode("23#1", "auto") in the same file builds exactly *4*311*#23#1##, _zone_address_and_name documents that form as what libqtdevices sends, and the firmware oracle thermo-ts10 suite shows all 12 gateways forward *4*311*#23#1## (9 ack/out, 3 nack/out, the same split as the *4*311*#1## the PR accepts). So the guard refused a frame the library itself builds. The same read showed the regex was unbounded (#999, #0#99, #01 built frames) while the sibling _WHO4_WHERE_REGEX of #140 is bounded, and that where was not stripped while actuators_status strips.

Live check (F461 2.0.11, 2026-10-26 16:45 UTC). *4*311*#2#1## is ACKed in 0.10 s with no bus event and zone 2 unchanged (a standalone zone, no central unit to act on), so the gateway accepts the form the fix now builds.

What changed. _WHO4_CENTRAL_WHERE_REGEX is #(?:0(?:#[1-4])?|[1-9][0-9]?(?:#[1-9])?) (#0, #0#1..4, #1..#99, #Z#1..9); where is stripped and quoted in the error; 41 test cases (was 21) including a parity test set_mode("23#1") == set_central_mode("#23#1"). Full suite 2080 passed on master 6c3930d, ruff and mypy clean. PR body and state diagram updated.

…review of OpenWebNet-HA#148)

The previous revision bounded `#0#C` to 1..4 and `#Z#C` to 1..9, reading
`#0#N` as "zone N of a 4-zone central unit". `#0#C` is the 4-zone unit's
configured zone selector, 1..99 (01..99): MyHOME_Suite OPEN.db
`#0#[ZA][ZB]`, MHCatalogue 4695 `ZAZB_CENTRAL` 1..99, and the oracle
`thermo-central-mode` rows (`*4*0*#0#2##` -> `D1 02 ...`, a two-nibble
central address byte); `#Z#C` is zone Z controlled through 4-zone central
unit C, also 1..99 (BtExperience parseControlUnit4 / parseZone4). Two-digit
virtual spellings (`michnovka#1`, `#0#05`) are the same `[ZA][ZB]` convention as
the plain `01` and are accepted. Only `#00`, `#0#0`, `#0#00` and
three-digit values are refused.

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

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Correction in 406d155, following @GreenGrassBlueOcean's #148 review: the previous revision bounded #0#C to 1..4 and #Z#C to 1..9, reading #0#N as "zone N of a 4-zone central unit". Per MyHOME_Suite OPEN.db (#0#[ZA][ZB]), MHCatalogue 4695 (ZAZB_CENTRAL 1..99), BtExperience (parseControlUnit4 / parseZone4) and the oracle thermo-central-mode rows (*4*0*#0#2## → D1 02 …), #0#C is the 4-zone unit's configured zone selector and #Z#C the zone controlled through central unit C, both 1..99 (01..99). The regex is now #(?:0(?:#(?:0[1-9]|[1-9][0-9]?))?|(?:0[1-9]|[1-9][0-9]?)(?:#(?:0[1-9]|[1-9][0-9]?))?): #0, #0#5, #0#05, #0#99, #01, #23#12 build; only #00, #0#0, #0#00 and three-digit values are refused. 49 test cases, suite 2088 passed on master 6c3930d, ruff / mypy clean; body and diagram updated.

…l-mode-address

# Conflicts:
#	OWNd/message/heating.py

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.

3 participants