Repository navigation
fix(heating): set_central_mode requires a central-unit address - #137
GreenGrassBlueOcean merged 1 commit into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@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 |
|
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? |
|
Done: the physical capture is split out as OpenWebNet-HA/MyHOME#799 ( |
…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>
844f4fb to
a6c4cdc
Compare
|
@gdluck please rebase |
a6c4cdc to
0b29827
Compare
|
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 How it was found. An adversarial read of the PR against its neighbours in Live check (F461 2.0.11, 2026-10-26 16:45 UTC). What changed. |
|
Correction in 406d155, following @GreenGrassBlueOcean's #148 review: the previous revision bounded |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Blocking set_central_mode("1", ...) from emitting *4*311*1## (nack/silent across all 12 emulated gateways and on live F461 2.0.11) makes sense as public builder hardening, even though MyHOME (climate.py) already only calls set_central_mode when self._central is true (#0 or #0#C).
However, the current contract in _WHO4_CENTRAL_WHERE_REGEX has two architectural issues in OWNd/message/heating.py:
1. #Z and #Z#C are zone/probe addresses (ControlledProbeDevice), not Central Unit addresses (ThermalDevice)
- In
libqtdevices(TS10_1_0_23) andown-firmware-oracle(findings/MyHomeServer1/thermo-ts10.md),#0(3550) and#0#C(4695) emit Central Unit bus telegrams ($06D1..0302C1..., command byteC1), whereas#23and#23#1emit zone bus telegrams ($06D1010302C2170000\r, command byteC2, zone0x17= 23 under central unit 1). OWNHeatingCommand.set_modealready owns zone modes on central-linked probes (set_mode(23, CLIMATE_MODE_AUTO)->*4*311*#23##andset_mode("23#1", CLIMATE_MODE_AUTO)->*4*311*#23#1##, logging"Setting zone 23 mode to 'auto'.").- Allowing
#Zand#Z#Cinset_central_modeintroduces two problems:set_central_mode("#23#1", CLIMATE_MODE_HEAT)builds*4*1*#23#1##(Central Unit winter seasonWHAT=1on a probe address, whichControlledProbeDevicenever sends).human_readable_logemits"Setting Central Unit #23#1 mode to 'auto' (code 311)."when#23#1is zone 23, not a central unit. (Note also thattests/test_who4_central_mode_address.py:74has the fields reversed as"#Z#C: probe C of zone Z"— in#23#1,23is the zone/probe and1is the central unit, matching_zone_number.)
set_central_modeis explicitly documented as "Set operation mode for Central Unit (3550 99-zone or 4695 4-zone)". Its validwherecontract should be#0or#0#C(Cin1..99/01..99, matchingMyHOMEand the golden corpus); callers addressing a central-linked zone#Zor#Z#Cshould useset_mode(...).
2. Inconsistency with the sibling Central Unit builders directly below (OWNd/message/heating.py)
set_central_temperature,set_central_program(*4*31PP*WHERE##),set_central_scenario(*4*32SS*WHERE##),set_central_weekend(*4*315#31PP*WHERE##),set_central_holiday(*4*33DDD#31PP*WHERE##),set_holiday_end_date(*#4*WHERE*#30*...##),set_holiday_end_time(*#4*WHERE*#31*...##), andset_timed_manual_end_time(*#4*WHERE*#32*...##) all default towhere: str = "#0", log"Setting Central Unit {where} ...", and still accept barewhere="1"without#(*4*3101*1##,*4*3201*1##, etc.).- If we enforce central-unit
wherevalidation onOWNHeatingCommand, let's share a single_validate_central_where(where)helper across the central-unit builders rather than guardingset_central_modein isolation.
3. Housekeeping
- Please update the PR body references (
OpenWebNet-HA/MyHOME#799is merged;#143is closed). - Please rebase cleanly onto
master(12fef0c8) without merge commitc82d88fand reset commit authorship one8e20fb/406d155(Mac Book <macbook@Macs-MacBook-Pro.local>).
…review of OpenWebNet-HA#137) `set_central_mode`'s `where` is the central unit itself: `#0` (3550, 99 zones) or `#0#C` with `C` 1..99 / 01..99 (4695), the forms MyHOME and the golden corpus use. `#Z` and `#Z#C` are zone / probe addresses (libqtdevices `ControlledProbeDevice`, bus command byte `C2`) that `set_mode` builds, so the builder now refuses them with a `ValueError` that names `set_mode`; it used to build `*4*1*OpenWebNet-HA#23#1##` and log "Setting Central Unit OpenWebNet-HA#23#1" for zone 23 under central unit 1. The guard is one shared `_validate_central_where(where, purpose)` in `OWNd/message/heating.py` (strip, fullmatch `#0(?:#(?:0[1-9]|[1-9][0-9]?))?`, raise naming the builder's purpose and quoting the stripped value) used by every central-unit builder that defaults to `where="#0"`: `set_central_mode`, `set_central_temperature`, `set_central_program`, `set_central_scenario`, `set_central_weekend`, `set_central_holiday`, `set_holiday_end_date`, `set_holiday_end_time` and `set_timed_manual_end_time`. Each now refuses a bare zone (`*4*3101*1##`, `*michnovka#4*1*OpenWebNet-HA#30*...##` used to be built), `#Z`, `#Z#C`, `#00`, `#0#0`, three digits, `None` and `True`, and accepts `#0` / `#0#C`. Tests: `tests/test_who4_central_mode_address.py` follows the contract (`#Z` / `#Z#C` refused, the parity test became "set_central_mode refuses what set_mode builds", the reversed "#Z#C: probe C of zone Z" comment is gone), the new `tests/test_who4_central_unit_where.py` covers every builder (`#0`, `#0#1`, `#0#12`, `#0#05` accepted; `1`, `michnovka#1`, `OpenWebNet-HA#23#1`, `#00`, `#0#0`, `#0#100`, `None`, `True` refused; whitespace; defaults), and the dimension 22 guard fixtures address the 4-zone central unit `#0#4` instead of `michnovka#4`. Statement coverage of `OWNd/message/heating.py` stays at 100 %. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Both architectural points are in 2583a43, thanks for the libqtdevices / thermo-ts10 reading:
Checks: 2302 passed, 100 % statements, One caveat worth your eye on the MyHOME side: Housekeeping: references updated (#799 merged, #143 closed). The clean rebase onto |
|
@gdluck please check ci failing for all prs in ownd |
|
Checked: the Fix: #150 adds |
…shared guard
`set_central_mode("1", mode)` built `*4*311*1##`, which is nack/silent on
all 12 emulated gateways and on a live F461 2.0.11: central-unit WHATs
address the central unit by API contract (OWNd#77 fix 3). The builder now
requires a central-unit address and the contract is shared by every
central-unit builder.
- `_WHO4_CENTRAL_WHERE_REGEX = #0(?:#(?:0[1-9]|[1-9][0-9]?))?` (ASCII):
`#0` (3550, 99 zones) or `#0#C` with C = 1..99 / 01..99 (4695, the
unit's configured zone, MyHOME_Suite OPEN.db `#0#[ZA][ZB]` and
MHCatalogue 4695 `ZAZB_CENTRAL`; oracle thermo-central-mode
`*4*0*#0#2##` -> `D1 02`). `#Z` and `#Z#C` are zone / probe addresses
(ControlledProbeDevice, bus command byte C2) handled by `set_mode`,
which already builds `*4*311*OpenWebNet-HA#23##` and `*4*311*OpenWebNet-HA#23#1##`; they are
refused with a ValueError that names `set_mode`.
- One `_validate_central_where(where, purpose)` (strip, fullmatch, quoted
error) is the first statement of all nine central-unit builders:
set_central_mode, set_central_temperature, set_central_program,
set_central_scenario, set_central_weekend, set_central_holiday,
set_holiday_end_date, set_holiday_end_time, set_timed_manual_end_time.
Signatures and frames are unchanged; each refuses a bare `1`, `michnovka#1`,
`OpenWebNet-HA#23#1`, `#00`, `#0#0`, `#0#100`, None and True, and accepts `#0`,
`#0#1`, `#0#12`, `#0#05` (whitespace stripped).
- Tests: tests/test_who4_central_mode_address.py (49 cases, incl.
"set_central_mode refuses what set_mode builds"),
tests/test_who4_central_unit_where.py (every builder, 120 cases); the
dimension-22 guard fixtures use `#0#4` instead of the no-longer-valid
`michnovka#4`.
Reviews 5642254816-era (OWNd#77), 5684988147 (OpenWebNet-HA#137) and the OpenWebNet-HA#148 review
on the selector bound are folded in; squashed on master 4213cab as asked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2583a43 to
938b98b
Compare
|
Housekeeping done: the branch is now a single commit |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks @gdluck — all points from review #5684988147 are cleanly addressed in 938b98b:
- Central Unit
WHEREcontract (#0/#0#C):_WHO4_CENTRAL_WHERE_REGEX(#0(?:#(?:0[1-9]|[1-9][0-9]?))?,re.ASCII) accurately restricts Central Unit builders (ThermalDevice, bus command byteC1) to3550(#0) and4695(#0#1..99/#0#01..99), while rejecting#Z/#Z#Czone/probe addresses (ControlledProbeDevice, bus command byteC2) with a clearValueErrorpointing toset_mode(...). - Consistent guard across all 9 builders: Sharing
_validate_central_where(where, purpose)acrossset_central_mode,set_central_temperature,set_central_program,set_central_scenario,set_central_weekend,set_central_holiday,set_holiday_end_date,set_holiday_end_time, andset_timed_manual_end_timecloses the bare-zone gap uniformly, backed bytests/test_who4_central_mode_address.py,tests/test_who4_central_unit_where.py, and the updated#0#4cases intests/test_who4_dimension22_guard.py. MyHOMEYAMLcentral: truenote: CheckedMyHOME(validate.py/climate.py) —climate_schematakeszone:(notwhere:), andMyHomeDeviceSchemaalready normalizeszone: <C>withcentral: trueto#0#<C>, so valid YAML central configs continue to pass_validate_central_where.- History & CI: Clean single commit on
master(4213cab),CHANGELOG.mdupdated, and all 8 CI gates (includingMyHOME test suiteandGolden corpus & smoke gates) are green.
(Minor non-blocking note: the ## State Diagram and ## Testing sections in the GitHub PR description still mention #Z | #Z#C from the earlier revision, though the commit message and docstrings in 938b98b are already updated.)
LGTM — ready to merge once marked Ready for review.
OWNd/__init__.py: __version__ = "2.0.0b12". CHANGELOG.md: [Unreleased] promoted to [2.0.0b12] - 2026-11-01 (every PR merged since v2.0.0-b11: #134, #135, #136, #137, #138, #139, #140, #142, #145, #147, #148, #150, #151) with a fresh empty [Unreleased] header. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Requested by A
Summary
OWNHeatingCommand.set_central_mode("1", CLIMATE_MODE_AUTO)built*4*311*1##: the builder accepted anywhere. 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#Cand#Z#Care 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#Zonly (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 isset_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/silenton 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_statusdeprecation-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]?))?(re.ASCII): the central unit only,#0(3550, 99 zones) or#0#CwithC= 1..99 / 01..99 (4695, the unit's configured zone), as MyHOME and the golden corpus use.#Zand#Z#Care zone / probe addresses (ControlledProbeDevice, bus command byteC2) and belong toset_mode(...), which already builds*4*311*#23##and*4*311*#23#1##;set_central_moderefuses them with aValueErrorthat namesset_mode. One shared_validate_central_where(where, purpose)(strip, fullmatch, quoted error) is the first statement of all nine central-unit builders:set_central_mode,set_central_temperature,set_central_program,set_central_scenario,set_central_weekend,set_central_holiday,set_holiday_end_date,set_holiday_end_time,set_timed_manual_end_time; signatures and frames are unchanged, each now refuses a bare1,#1,#23#1,#00,#0#0,#0#100,None,Trueand accepts#0,#0#1,#0#12,#0#05. History: squashed into one commit on master 4213cab (938b98b) with the authorship reset, as asked in review 5684988147; the earlier revisions (e8e20fb,406d155,2583a43) are folded into it.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 }Where are the traces
#Zformtests/fixtures/traces/f461_live_2026-10-15(merged), runsrun3_climateandprobe_followup*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)OpenWebNet-HA/MyHOME#799(merged),f461_live_2026-10-23run 1 (run1_oracle_replay_readonly) andmyhomeserver1_live_2026-10-24run 2 (run2_reads_thermo_sound_alarm_lights)*#13**16*3*87*13##)*#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-modeandownd-pr82-heating(results/<P>/<V>/oracle/full/<suite>.tsv; the rows are identical in both where both carry the frame). Twelve gateways: F450020010, F453010100, F453AV030014, F454020051, F459020105, F460020012, F461020011, H4684020054, MH200020102, MH200N010108, MH202010024, MyHomeServer1028206.ack/outnack/out(forwarded, no central unit on the emulated bus)nack/silent*4*311*1##(plain, this builder before the fix)*4*311*#1##(central form, this builder)*4*1*1##(plain heating)*4*0*1##(plain conditioning)*4*303*1##(plain off)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#2forms, table above).Revision 938b98b (review 5684988147; squashed on master 4213cab). Contract narrowed to
#0/#0#C; shared_validate_central_whereacross the nine central-unit builders;tests/test_who4_central_mode_address.pyrewritten (the parity test became "set_central_moderefuses whatset_modebuilds", the reversed#Z#Ccomment is gone) andtests/test_who4_central_unit_where.pyadded (every builder, 120 tests);tests/test_who4_dimension22_guard.pyheating fixtures use#0#4instead of the no-longer-valid#4. Suite 2302 passed, 100 % statements,ruff/mypy/ golden corpus / library standards / docs check clean; MyHOMEv2-phase1-architecturesuite against this head 5044 passed. One caveat for MyHOME found while checking callers:climate.pycallsset_central_mode/set_central_temperatureonly underself._central, which yamlcentral: truecan force on a non-central WHERE (where: "2"); such a config now raises instead of building*#4*2*#14*…##(bus-discovered devices are central only for#0/#0#1). No MyHOME test covers that path; not changed here. Rebased onto master4213cabas a single re-authored commit.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 onwhere="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,#0with 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.master17 fail, 4 pass (set_central_mode('1', 'auto')returns*4*311*1##,#1#2is accepted); on this branch all 49 pass.master6c3930d 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, head406d155):2088 passed, 1 skipped; 100 % statement coverage (scripts/verify_coverage.py),mypy OWNd tests/typingclean,ruff checkclean,scripts/sync_documentation.py --checkandscripts/verify_library_standards.pypass.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 (closed).🤖 Generated with Claude Code