Skip to content

fix(#77): review follow-ups (fan zone, brightness log, WHO 25 commands) and golden corrections - #83

Merged
GreenGrassBlueOcean merged 6 commits into
masterfrom
fix/issue-77-review-followups
Oct 7, 2026
Merged

GreenGrassBlueOcean merged 6 commits into
masterfrom
fix/issue-77-review-followups

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-ups from the review of #82: code fixes from the audit, golden fixtures that the firmware data and the plant captures contradict, addressing all review nits, and achieving full protocol parity with BTicino's 10-inch Touch Screen reference client (libqtdevices tag TS10_1_0_23) (f2df920). Rebased cleanly onto master following the release of 2.0.0-b10 (#78).

1. Protocol Parity with libqtdevices TS10_1_0_23 (f2df920)

Empirical parity with BTicino's reference touch screen stack (ControlledProbeDevice, NonControlledProbeDevice, ThermalDevice, and ThermalDevice4Zones):

Method / Feature OpenWebNet Frame libqtdevices Reference
set_mode / set_protection Antifreeze *4*102*#Z##, generic protection *4*302*#Z##, cooling protection *4*202*#Z## ControlledProbeDevice::setProtection
get_fan_speed *#4*Z*#11*2## ControlledProbeDevice::requestFancoilStatus
get_probe_temperature External sensor *#4*Z*14*P## NonControlledProbeDevice::requestStatus
set_central_program(p) *4*3100+p*#0## (programs 1..16) ThermalDevice::setWeekProgram (test_thermal_device.cpp:94 tests p=13)
set_central_scenario(s) *4*3200+s*#0## (scenarios 1..16) ThermalDevice::setScenario (test_thermal_device.cpp:325 tests s=12)
set_central_weekend(p) *4*315#3100+p*#0## ThermalDevice::setWeekendDateTime (test_thermal_device.cpp:86 tests p=12)
set_central_holiday(d, p) *4*33000+d#3100+p*#0## ThermalDevice::setHolidayDateTime (test_thermal_device.cpp:78 tests d=2, p=15)
set_timed_manual(t, h) *4*312#TTTT#h*#WHERE## ThermalDevice4Zones::setManualTempTimed (thermal_device.cpp:371)
set_holiday_end_date(d) *#4*#0*#30*DD*MM*YYYY## ThermalDevice::setHolidayEndDate (test_thermal_device.cpp:108)
set_holiday_end_time(t) *#4*#0*#31*HH*MM## ThermalDevice::setHolidayEndTime (test_thermal_device.cpp:115)
set_timed_manual_end_time(t) *#4*#WHERE*#32*HH*MM## ThermalDevice4Zones::setEndTime (test_thermal_device.cpp:301)
OWNHeatingEvent parsing Decodes timed manual (112/212/312), weekly programs (3101..3116), scenarios (3201..3216), holidays (33001..33255), and dimension 30/31/32 end dates/times ThermalDevice::parseFrame (test_thermal_device.cpp:232-245)

2. Code Fixes & Address Parsing (853b23d, 83c4756)

Finding Change Test
Fan-speed & compound zone parsing set_fan_speed, set_mode, and set_temperature take the zone from the first field via shared _zone_number: #23#1 and 23#1 resolve to zone 23. In 83c4756, _zone_number strictly validates addresses, rejecting ##..., negative numbers, non-digits, and multi-part addresses like #0#1#2 with ValueError. Matches ControlledProbeDevice::setFancoilSpeed (*#4*23*#11*3##). test_fix2_fan_speed_takes_the_zone_from_the_first_field, test_zone_builders_take_the_zone_from_the_first_field, test_zone_builders_refuse_invalid_addresses
Capped brightness logging set_brightness logs the level it sends: 120 now logs brightness to 100% (requested 120%). Frame remains *#1*W*#1*200*S##. test_fix7_log_shows_the_level_that_is_sent
WHO 25 command parsing _parse_who25_command follows the event rule: WHAT 31/32 and *#25* status requests are OWNDryContactCommand, 21–28 are OWNCenPlusCommand, and any other numeric WHAT is a plain OWNCommand. test_fix9_command_parser_uses_the_same_who25_rule

3. Golden Corpus Corrections (6021f2a, 83c4756)

Corpus checked against OWNd#77 firmware data, real-gateway probes (own-firmware-oracle), and MyHOME plant traces. All 48 community-plant-capture fixtures pass. Corrections to document-derived fixtures:

Fixture Was Now Evidence
thermo.cmd.mode.antifreeze.zone1 *4*110*1## *4*102*#1## 110 is manual heating (WIN_MANUAL). Antifreeze is *4*102*#WHERE##.
thermo.cmd.mode.protection.zone1 *4*111*1## *4*302*#1## 111 is automatic heating. Generic protection is *4*302*#WHERE##.
thermo.cmd.mode.thermal_protection.zone1 (new) — *4*202*#1## WHAT 202 is Cooling Protection for zone setup (*4*202*#WHERE##).
cover.cmd.up.bus.21, cover.cmd.stop.bus.21 21#4#1 21#4#01 Firmware refuses one-digit interface; Encyclopedia WHO 2: INTERFACE = [0-1][1-9].
dry_contact.event.on.15 → .339 *25*31#1*15## *25*31#1*339## Bus captures on MH200N, MH202, F454, H4890 show WHERE starting with 3.
thermo.cmd.central.status.cu99 *#4*#0*14## *#4*#0## F454 and MyHomeServer1 reject dimension 14 on central unit (#629). Retained as …cu99.dim14_legacy with deprecation note.
cen.event.extended_press.btn3.0512 (new) — *15*03#3*0512## Corroborated by MH200N + MyHomeServer1 traces (#624).

4. Review Follow-ups (a8614c2, 83c4756, f2df920)

  • Zone antifreeze & protection tied to builders: set_central_mode("#1", "antifreeze") and ("#1", "protection") build *4*102*#1## and *4*302*#1##.
  • central_status() deprecated: Raises DeprecationWarning pointing to status(where) and documents F454/MyHomeServer1 dimension 14 drop/NACK behavior.
  • Strict compound address validation: Three-part addresses (e.g. #0#1#2) raise ValueError.
  • Fixture count: corpus.json has 118 entries.

Downstream

Companion PR OpenWebNet-HA/MyHOME#643 has been merged into v2-phase1-architecture. Both Golden corpus & smoke gates and MyHOME test suite pass cleanly (3,452 passed, 1 skipped).

Verification

  • 1,234 passed locally. 100.0% statement and branch coverage on heating.py.
  • ruff check, mypy OWNd, and tools/golden/validate_corpus.py (118 frames) clean.

@codecov-commenter

Copy link
Copy Markdown

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

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

GreenGrassBlueOcean added a commit to OpenWebNet-HA/MyHOME that referenced this pull request Oct 6, 2026
Same corrections as OpenWebNet-HA/OWNd#83 in MyHOME's copy of the corpus: antifreeze and protection zone commands (*4*102*#1##, *4*302*#1## instead of 110/111), the two-digit local-bus interface for cover 21, the H4890 dry-contact capture *25*31#1*339## instead of *25*31#1*15##, a note on the disputed CEN WHERE 0001 and the MH200N + MyHomeServer1 capture *15*03#3*0512## (MyHOME#624). SOURCE.yaml lists the new captures; README and corpus.json follow (184 fixtures). Passes with OWNd 2.0.0b9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GreenGrassBlueOcean GreenGrassBlueOcean changed the title fix(#77): review follow-ups for fan zone, capped brightness log, WHO 25 commands fix(#77): review follow-ups (fan zone, brightness log, WHO 25 commands) and golden corrections Oct 6, 2026
GreenGrassBlueOcean added a commit that referenced this pull request Oct 6, 2026
…l_status, share zone parsing

Review of #83:

- thermo.cmd.mode.antifreeze.zone1 / .protection.zone1 now have builder
  blocks: set_central_mode("#1", "antifreeze") and ("#1", "protection")
  build *4*102*#1## and *4*302*#1##. set_mode only builds 303 and 311, so
  without these nothing pinned what a caller gets for antifreeze or
  protection.
- central_status() raises a DeprecationWarning pointing to status(where).
  It still builds *#4*WHERE*14## for existing callers, which an F454
  (MyHOME#629) and a real MyHomeServer1 refuse; status("#0") is the poll
  the F454 answers. thermo.cmd.central.status.cu99.dim14_legacy loses its
  builder block, so the corpus no longer parity-tests a deprecated method,
  and its note says roundtrip only means OWNd re-serializes it exactly.
- set_mode and set_temperature take the zone from the first WHERE field,
  through the same _zone_number helper as set_fan_speed: "#23#1" was zone 1
  and "23#1" raised ValueError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from fix/issue-77-gateway-protocol-defects to master October 6, 2026 20:09
GreenGrassBlueOcean added a commit to OpenWebNet-HA/MyHOME that referenced this pull request Oct 6, 2026
Same corrections as OpenWebNet-HA/OWNd#83 in MyHOME's copy of the corpus: antifreeze and protection zone commands (*4*102*#1##, *4*302*#1## instead of 110/111), the two-digit local-bus interface for cover 21, the H4890 dry-contact capture *25*31#1*339## instead of *25*31#1*15##, a note on the disputed CEN WHERE 0001 and the MH200N + MyHomeServer1 capture *15*03#3*0512## (MyHOME#624). SOURCE.yaml lists the new captures; README and corpus.json follow (184 fixtures). Passes with OWNd 2.0.0b9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GreenGrassBlueOcean and others added 3 commits October 6, 2026 23:33
…25 commands

Three small findings from the audit of OWNd#82:

- set_fan_speed takes the zone from the first WHERE field. "#23#1"
  built zone 1 (the last field). libqtdevices sends the plain zone for a
  probe addressed #23#1: *#4*23*#11*3##. "23#1" now gives zone 23
  instead of "Invalid zone address". #0 and #0#N are still refused.
- set_brightness logs the level it sends: 120 logs
  "brightness to 100% (requested 120%)" instead of "120%". The frame is
  unchanged (*#1*W*#1*200*S##).
- The WHO 25 command parser follows the event parser's rule from fix 9:
  only WHAT 31/32 (and *#25* status requests) are OWNDryContactCommand,
  21..28 stay OWNCenPlusCommand, and any other numeric WHAT is a plain
  OWNCommand. A non-numeric WHAT stays a dry contact, as in the event
  parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntradict

Found while checking the golden corpus against the OWNd#77 firmware data
(gdluck's sweeps and gateway probe, own-firmware-oracle) and the MyHOME
plant traces. None of the community captures disagreed; these document-
derived fixtures did. Each corrected entry names its new source, as the
golden README requires.

- thermo.cmd.mode.antifreeze.zone1: *4*110*1## -> *4*102*#1##, and
  thermo.cmd.mode.protection.zone1: *4*111*1## -> *4*302*#1##. 110 and
  111 are manual and automatic heating (Encyclopedia what.md, libqtdevices
  WIN_MANUAL / WIN_AUTOMATIC); the firmware drops 110 and treats 111 as
  automatic. The Encyclopedia zone-setup table gives *4*102*#WHERE## and
  *4*302*#WHERE##, and libqtdevices sends 302 to #zone.
- cover.cmd.up.bus.21 / cover.cmd.stop.bus.21: interface 1 -> 01. The
  firmware refuses a one-digit interface (au2.own) and the Encyclopedia
  WHO 2 grammar is INTERFACE = [0-1][1-9].
- dry_contact.event.on.15 -> dry_contact.event.on.339, the H4890 capture
  *25*31#1*339## (MyHOME#466): every captured dry contact has a WHERE that
  starts with 3, as in the firmware's bt_luci rule.
- thermo.cmd.central.status.cu99 is now the F454 capture *#4*#0## (status()
  builder, MyHOME#629). The old *#4*#0*14## stays as
  thermo.cmd.central.status.cu99.dim14_legacy so central_status() keeps a
  parity check, with the note that gateways refuse it. Mirrors MyHOME's
  golden copy.
- cen.event.extended_press.btn1.0001 keeps its frame but notes the dispute
  (the spec allows 0001, bt_luci refuses it). New capture
  cen.event.extended_press.btn3.0512 (*15*03#3*0512##, MH200N +
  MyHomeServer1, MyHOME#624) with OWNCenCommand builder parity.

SOURCE.yaml lists the three new captures; README and corpus.json follow
(117 fixtures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l_status, share zone parsing

Review of #83:

- thermo.cmd.mode.antifreeze.zone1 / .protection.zone1 now have builder
  blocks: set_central_mode("#1", "antifreeze") and ("#1", "protection")
  build *4*102*#1## and *4*302*#1##. set_mode only builds 303 and 311, so
  without these nothing pinned what a caller gets for antifreeze or
  protection.
- central_status() raises a DeprecationWarning pointing to status(where).
  It still builds *#4*WHERE*14## for existing callers, which an F454
  (MyHOME#629) and a real MyHomeServer1 refuse; status("#0") is the poll
  the F454 answers. thermo.cmd.central.status.cu99.dim14_legacy loses its
  builder block, so the corpus no longer parity-tests a deprecated method,
  and its note says roundtrip only means OWNd re-serializes it exactly.
- set_mode and set_temperature take the zone from the first WHERE field,
  through the same _zone_number helper as set_fan_speed: "#23#1" was zone 1
  and "23#1" raised ValueError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the fix/issue-77-review-followups branch from 29040c9 to a8614c2 Compare October 6, 2026 21:34

@fedem95 fedem95 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 zone-parsing row cites libqtdevices TS10_1_0_23 (setFancoilSpeed). In that library, probe_device.h:156 documents the probe address as "probe#central", and test/test_probe_device.cpp creates the device as ControlledProbeDevice("23#1", "1", "23", ...) (line 111), then expects:

  • fan speed: *#4*23*#11*3## (line 139), plain zone;
  • automatic: *4*311*#23#1## (line 132), full address;
  • manual setpoint: *#4*#23#1*#14*0250*3## (line 125), full address.

On 83c4756 the address #23#1 gives:

  • set_fan_speed("#23#1", 2) → *#4*23*#11*2## (master: *#4*1*#11*2##);
  • set_mode("#23#1", "auto") → *4*311*#23## (master: *4*311*#1##);
  • set_temperature("#23#1", 22.5, "heat") → *#4*#23*#14*0225*1## (master: *#4*#1*#14*0225*1##).

So the fan-speed frame matches libqtdevices, while set_mode and set_temperature drop the #1 that libqtdevices keeps. Should those two keep the full address? The addresses 1, #1, #5, 99, #0 and #0#1 give the same frames as on master.

Other checks on 83c4756:

  • *25*11#121*10## parses as OWNCommand (master: OWNDryContactCommand); *25*31#1*339##, *25*32#0*33## and *#25*339## parse as OWNDryContactCommand, *25*21#1*21## as OWNCenPlusCommand.
  • set_brightness("31", 120) builds *#1*31*#1*200*0## and logs "brightness to 100% (requested 120%)".
  • central_status() emits a DeprecationWarning; MyHOME's custom_components has no call to it.
  • Every dry-contact frame (*25*31/*25*32) in MyHOME's tests/fixtures/traces has a WHERE starting with 3 (31, 33, 331–339).
  • OWNd suite: 1226 passed, 2 skipped (test_metadata excluded, my local env has an old OWNd installed). MyHOME v2-phase1-architecture (394c028) against 83c4756, locally: 3452 passed, 1 skipped.

…ture

Review of #83 (fedem95): libqtdevices TS10_1_0_23 test_probe_device.cpp (line 111, ControlledProbeDevice) expects plain zone for fan speed (*#4*23*#11*3##), but retains the full #probe#central address for automatic (*4*311*#23#1##) and setpoint (*#4*#23#1*#14*0250*3##).

Use _zone_address_and_name helper to retain compound addresses (#23#1, 23#1, #0#1) for set_mode and set_temperature, while keeping _zone_number extracting the plain zone for set_fan_speed.
…0_23

- Support protection, antifreeze, and thermal_protection in set_mode and set_protection matching ControlledProbeDevice::setProtection.
- Add get_fan_speed matching ControlledProbeDevice::requestFancoilStatus.
- Support external sensor parameter in get_probe_temperature matching NonControlledProbeDevice::requestStatus.
- Add set_central_program, set_central_scenario, set_timed_manual, set_central_weekend, set_central_holiday, and dimensions 30/31/32 end date/time methods matching ThermalDevice.
- Support timed manual (112, 212, 312), program (3101..3116), scenario (3201..3216), and holiday (33001..33255) modes and dimensions 30/31/32 in OWNHeatingEvent.
- Update who04_thermo.yaml fixtures to use set_mode for protection modes.
- 100.0% statement and branch coverage on heating.py.
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Good catch on probe#central address preservation! You're completely right: libqtdevices (ControlledProbeDevice / test_probe_device.cpp:111-139) preserves the full #probe#central address for automatic mode (*4*311*#23#1##) and manual setpoint (*#4*#23#1*#14*0250*3##), while extracting the plain zone for fancoil commands (*#4*23*#11*3##).

Taking a step back, libqtdevices is the reference C++/Qt stack from BTicino's 10-inch Touch Screen display (TS10 firmware). Since the 10" display acts as a full-featured central console on the OpenWebNet bus, we used this opportunity in commit f2df920 to audit the 10" screen's entire thermo device model and align OWNd's WHO 4 subsystem across all three device types defined for the display:

  1. Controlled Probes (ControlledProbeDevice):

    • set_mode and set_temperature now preserve compound addresses (#23#1 and 23#1 -> #23#1), generating *4*311*#23#1## and *#4*#23#1*#14*0225*1## exactly as the 10" panel expects.
    • set_fan_speed and the new get_fan_speed extract the zone number (*#4*23*#11*3## and *#4*23*11##).
    • set_mode now supports "protection" (302), "antifreeze" (102), and "thermal_protection" (202) with automatic # prefixing for central-unit routed commands (*4*302*#WHERE##). A convenience method set_protection was also added.
  2. Read-Only & External Probes (NonControlledProbeDevice):

    • get_probe_temperature now supports an optional sensor parameter (*#4*WHERE*15#1##), matching the 10" screen's method for reading external radio temperature sensors.
  3. Central Units (ThermalDevice - 4-zone & 99-zone):

    • Added support for weekly programs (3101..3116 via set_central_program), preset scenarios (3201..3216 via set_central_scenario), timed manual mode (*4*312#TTTT#H*WHERE##), weekend mode (315), and holiday mode (33DDD).
    • Added write commands and event parsing for dimension 30 (holiday end date), 31 (holiday end time), and 32 (timed manual end time).
    • OWNHeatingEvent now parses all these extended central-unit modes and dimensions.
  4. Corpus & Quality Gates:

    • Golden corpus fixtures for zone protection modes now use set_mode directly.
    • Maintained 100.0% statement and branch coverage on heating.py (3,818 / 3,818 statements covered across the repo).
    • All CI workflows (including the downstream MyHOME test suite) are passing green.

@fedem95 fedem95 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, the compound address now matches libqtdevices: for #23#1 and 23#1, set_mode auto/off build *4*311*#23#1## / *4*303*#23#1##, set_temperature builds *#4*#23#1*#14*0225*1##, fan speed stays *#4*23*#11*2##. Plain addresses (1, #1, #5, 99, #0, #0#1, 0) build the same frames as master, with and without standalone; a three-part address like #0#1#2 now raises ValueError (master turned it into zone 2).

Checked f2df920's new central-unit builders against test/test_thermal_device.cpp: set_central_program(13) → *4*3113*#0## (l. 94), set_central_scenario(12) → *4*3212*#0## (l. 325), set_central_weekend(12) → *4*315#3112*#0## (l. 86), set_central_holiday(days=2, program=15) → *4*33002#3115*#0## (l. 78), end date/time *#4*#0*#30*13*02*2009## / *#4*#0*#31*23*31## (ll. 108–120), timed end *#4*#1#2*#32*13*05## (l. 301). The dimension 30/31/32 parsers decode the frames of ll. 232–245.

Notes:

  • The PR description doesn't list f2df920's additions; only your comment does.
  • In libqtdevices the #2 in *4*312#TTTT#2 and the 2 days in *4*33002#… are commented as dummy values (thermal_device.cpp:371, :166); the end is always set by the following dimension 32 / 30+31 frames. Here hours and days are exposed as real durations, in separate methods.
  • set_central_program accepts 1..16; libqtdevices has no range check there, and its enum comments describe weekly programs as "1 out of 3" (ll. 56, 66). Where does 16 come from?

OWNd suite 1234 passed locally; MyHOME v2-phase1-architecture (394c028) against f2df920: 3452 passed, 1 skipped.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review and verification against v2-phase1-architecture!

Regarding your notes:

  1. PR Description: Updated the PR description to document the f2df920 additions (the central-unit builders, protection modes, fan speed, external probe, dimensions 30/31/32, and event decoding).

  2. Dummy hours (#2) & days (2):
    In thermal_device.cpp (ll. 166, 371), the TS10 UI hardcodes 2 as a placeholder because the protocol grammar requires a duration parameter (312#TTTT#HOURS and 33DDD#PROGRAM), while the UI subsequently defines the boundary with absolute Dimension 32 or Dimension 30/31 frames. In OWNd, keeping hours and days exposed allows callers to trigger simple duration-based manual/holiday mode without emitting absolute clock frames, while callers specifying end datetimes can call the companion dimension methods.

  3. Program range 1..16 vs "1 out of 3":
    The "1 out of 3" comment in thermal_device.cpp reflects the 4-zone regulator (which had 3 preset programs on its local keypad). The 99-zone central units (3550 / 3456) support 16 programs (3101..3116) and 16 scenarios (3201..3216). Furthermore, test/test_thermal_device.cpp itself exercises programs well outside 1..3:

    • line 78: setHolidayDateTime(..., 15) → *4*33002#3115*#0## (Program 15)
    • line 86: setWeekendDateTime(..., 12) → *4*315#3112*#0## (Program 12)
    • line 94: setWeekProgram(13) → *4*3113*#0## (Program 13)
      Allowing 1..16 guarantees compatibility across both 4-zone and 99-zone systems.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit c7c3a7b into master Oct 7, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the fix/issue-77-review-followups branch October 7, 2026 11:50
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