Repository navigation
fix(protocol): resolve protocol and parser defects from MHS1 firmware analysis (#77) - #82
Conversation
… analysis (#77) - Fix 1: Align central mode WHAT codes to canonical 303 (off), 1 (heat), 0 (cool), 311 (auto), 102 (antifreeze), 202 (protection) - Fix 2: Strictly require plain zone (1..99) and speed (0..3) in set_fan_speed, rejecting '#' central addresses - Fix 3: Return None for CLIMATE_MODE_AUTO on standalone zones (dropped by gateway) - Fix 4: Guard dimension 20 actuator status against empty where_param IndexError - Fix 5: Guard dimension 19 valve status against empty field ValueError - Fix 6: Support dimension parameters in _DIMENSION_REQUEST regex and extract _dimension_param - Fix 7: Route brightness <= 0 through switch_off and clamp command level to 101..200 - Fix 8: Recognize private bus routing (#4#NN) in interface property for WHO 0 and WHO 14 - Fix 9: Fall back to generic OWNEvent for non-31/32 WHO 25 frames rather than misclassifying as contact OFF - Fix 10: Correct canonical frame examples in protocol conformance matrix - Fix B1: Set supports_audio=False and omit WHO 16 from supported_who in MyHomeServer1Profile (MHS1 exclusively uses WHO 22) - Synchronize golden thermo fixtures, regenerate corpus.json, and maintain 100.0% coverage across all 24 modules
Replace the single combined test with tests/test_issue_77_firmware_findings.py. Each test uses the frames from the source it names, verbatim: gdluck's MyHomeServer1 firmware run (au1-au4.tsv, OWNd#77 README), the read-only gateway probe, the OpenWebNet Encyclopedia, or MyHOME plant captures (#453, #466, #541). 69 of the new cases fail on a6eda4d, covering every item (fixes 1-10 and B1); the rest pin captured frames that must keep decoding. - Golden: the four central-mode fixtures changed in the previous commit still claimed source legrand-spec. They now cite the Encyclopedia WHO 4 table (what.md: 0, 1, 102, 202, 303, 311) and the firmware run, as the golden README requires for a corrected fixture. corpus.json regenerated. - Matrix: WHO 15 has captures (MyHOME#541 press and short release), so the row is VERIFIED with those frames plus the firmware-checked #2/#3 forms; the footnote names the source of every changed example. - README/sound.py: sound.py implements WHO 16 only (gdluck's README note). - Restore the turn_off assertion dropped from test_message_exhaustive.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ring, merge master B1 rested on emulation alone and would make MyHOME skip WHO 16 discovery and the media_player platform for MyHomeServer1 users with a sound system. master already marks MyHomeServer1 sound as unmeasured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Live test: fix 7 (brightness 0) on a real F414 dimmerGateway: MH200 at 192.168.1.40 (fw 2.1.0). Device: bureau dimmer, point 79 (F414), on at 100% (
So the F414 on an MH200 behaves like the MyHomeServer1 firmware emulation: level 100 is not accepted, switch-off is. Not tested live: the cap at level 100 (a request above 100 was not sent), and the other fixes. Fix 1 (central unit modes) was not written to the heating plant. Also pushed to this branch: master merged, B1 (MyHomeServer1 without WHO 16) removed (see the PR description), and the actuator fallback kept as the string |
CI: the two MyHOME conformance checks are expected to fail on this PR
The downstream change is MyHOME#630. It cannot merge until OWNd 2.0.0b10 is on PyPI, and b10 needs this PR, so the two block each other. Evidence that the pair fits: conformance run 37348391705, dispatched on this branch with Proposal: merge this PR with those two required checks overridden, citing the run above, then release b10 and merge #630. All other checks pass. |
Firmware oracle: the PR's builders against MyHomeServer1 2.82.06 and MH200N 1.1.8The frames come from OWNd's own builders, master (f7894c4) vs this PR (6038ceb), not copied by hand. Suites, results and hashes: own-firmware-oracle#10. Each suite re-run gives zero diff. Status Heating, MyHomeServer1 2.82.06 (
Lights, both images (
On MyHomeServer1 the reply column reads Not covered: heating on MH200N (its oracle target has no |
|
CI on own-firmware-oracle#10 (the firmware oracle results above) is green. |
|
Important Correction, 2026-10-06 (after fedem95's and gdluck's reviews). Several statements below are wrong. None of them changes the code in this PR.
Empirical cross-validation of OWNd#77 across MH200N (010108) and MyHomeServer1 (028206)Following the initial oracle run (own-firmware-oracle#10), an exhaustive cross-gateway empirical replay of all 10 OWNd#77 audit suites was executed against the MH200N gateway ( All suites, raw TSVs, checks, and the hash-pinned index are published in own-firmware-oracle#11. 1. Cross-Gateway Comparison Matrix (All 10 Fixes)
2.
|
fedem95
left a comment
There was a problem hiding this comment.
Thanks for running the MH200N image. A few things in this comment don't match the data:
- Section 2 (MH200N NACKs WHO 4 / WHO 18 because nothing handles them). For WHO 18 the real MH200N on 1.1.8 says otherwise: in MyHOME
tests/fixtures/traces/issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json(caiosweet's plant), each*#18*5x*51##request (51–57) gets its*#18*5x*51*VALUE##reply within ~150–400 ms. For WHO 4 the same trace only shows the gateway relaying bus events (zone temperatures,*#4*Z#1*20*0##); the*#4*0##request has no reply in the trace, and there are no ACK/NACK records, so it says nothing either way about WHO 4 requests or commands. Please limit section 2 to what the emulator showed, so nobody drops WHO 18 (or WHO 4) from the MH200N profile based on it. - Fix 7 row. The PR doesn't clamp to 101: it caps at level 100 (
*#1*31*#1*200*0##for 120) and sends switch-off for 0. "Universal" rests on two emulated images plus one live F414 on an MH200; I'd say "confirmed on MH200 (live), MH200N and MyHomeServer1 (emulated)". - Fixes 4, 5, 9. 4 and 5 change how OWNd parses frames the gateway emits, so a NACK when sending them as commands doesn't test them. Row 9 tests WHAT 21/23 (CEN+), but the code change only affects WHAT outside 21–28 and 31/32. So "this closes the Not covered items" doesn't hold for 4, 5 and 9, nor for 1–6 on the MH200N, which all got NACK.
None of this changes the code in this PR: run 37348391705 on 6038ceb passes both conformance jobs. I'd like the description to stop citing the daemon analysis as evidence before this is merged.
OWNd PR #82 reviewed against libqtdevices (TS10_1_0_23) and the mhs1 firmware auditReviewed 2026-10-06. PR head Legend: FW = firmware sweep result recorded in the audit; TS10 = BTicino's own touch-screen client library (libqtdevices); deduction = my inference, not a quoted fact. 1. Fix by fix
2. Doubts, checked against probe_device.cpp2.1
|
…devices "protection" mapped to 202, the summer protection, which also switches the plant to summer. BTicino's touch-screen client (libqtdevices TS10_1_0_23, thermal_device.cpp and test/test_thermal_device.cpp) sends 302, generic protection, to the central unit; its enum names 102 winter, 202 summer and 302 generic protection. gdluck's sweep lists 302 among the central WHATs the firmware forwards (AUDIT A1). "antifreeze" stays 102, the winter protection that the MyHomeServer1 captures in the golden corpus report as antifreeze. The golden notes for the 99-zone central off/heat/cool fixtures now cite the libqtdevices test that sends the same frames (gdluck's review, 2.1 and 2.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng None set_mode(AUTO, standalone=True) returned None, so MyHOME sent nothing and logged nothing. MyHOME flags every zone that is not a central unit standalone by default (climate.py: CONF_STANDALONE defaults to "not central"), so this dropped AUTO for zones that do run under a central unit, where *4*311*#Z## works. The firmware forwards 311 only in the # form (AUDIT A3) and BTicino's client sends that form (libqtdevices ControlledProbeDevice::setAutomatic, *4*311*#23#1##), so AUTO now ignores standalone. OFF keeps following it: *4*303*Z## works in both forms. gdluck's review, option (a) of 2.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @fedem95 and @gdluck. Changes in 2bc6c32, 3014896 and 885b8e4: gdluck
fedem95: all three points hold, and I've corrected the cross-validation comment in place (see the correction at the top). The PR description no longer cites the daemon analysis. Point 1 goes further than the trace shows: the MH200N's own Golden corpus versus the firmware data. I checked the fixtures against gdluck's sweeps and probe, the oracle results and the MyHOME traces. The firmware data contradicts none of the 48 plant captures, though many of them (alarm, dimension 7, covers behind an interface) it doesn't cover at all. The CEN+ capture
|
|
Re-ran the cross-check myself, since the dispatched run (37348391705) is on 6038ceb. MyHOME #630 (b23cb67) suite against this PR at 885b8e4, same pytest invocation as the conformance job but run locally (macOS, Python 3.14, HA 2026.9.2, OWNd taken from the PR tree via PYTHONPATH rather than pip-installed): 3380 passed, 2 failed:
Since 3014896, The rest checks out on 885b8e4: OWNd suite 1196 passed ( |
|
@fedem95, thanks for re-running it. Both points are done:
From the separate audit of this PR, I've changed the description only, with no push, so this head and the run above stay valid. It no longer says that antifreeze and protection read back as themselves: the parser has always read 102/302 as #630 waits for OWNd 2.0.0b10 on PyPI. The pin bump is ready to push as soon as it is published. |
fedem95
left a comment
There was a problem hiding this comment.
Approving. Checked against MyHOME #630 at de8cf4b3 locally (OWNd from this PR at 885b8e4 via PYTHONPATH, macOS, Python 3.14, HA 2026.9.2): full suite 3475 passed, 1 skipped, 0 failed; golden conformance 569 passed, 1 skipped. #630 now sends AUTO for standalone zones and the "command dropped" branch is gone. Conformance runs 37513598943 and 37518042220 on 885b8e4 both passed.
Also confirmed the parser reads *4*102*#0## and *4*302*#0## as off, as the updated description says. Fine with merging over the two required MyHOME checks given the cross-run, then b10 and #630.
Summary
Resolves #77. gdluck ran the MyHomeServer1 translator programs (
bt_luci,coso,bt_termo,bt_supervisione,bt_device,bt_multi) on an emulated SCS bus and found ten OWNd defects. This PR fixes them, B1 (dropping WHO 16 from the MyHomeServer1 profile) was removed from this PR: see Not included.Sources
au1.tsv–au4.tsvin gdluck's zip on #77. The older sweeps (oracle2/dl*.tsv) are quoted in the README but not published.gateway_probe.txtin the ziptests/fixtures/traces.libqtdevicestagTS10_1_0_23(thermal_device.cpp,probe_device.cppand their tests), as pointed out by gdluck.Items, sources and tests
All tests are in
tests/test_issue_77_firmware_findings.py. Each test uses the source frames verbatim and names its source in its docstring. 68 of its 180 cases fail on master (f7894c4), and every item has at least one of them. The other cases check that frames captured on real plants still decode the same way.set_central_mode: off 303, heat 1, cool 0, auto 311, antifreeze 102, protection 302 (was 100/101/102/103/110/111)#0and#0#2(own-firmware-oracle#10); 302 is in gdluck's list of accepted central WHATs (audit A1). BTicino client: sends*4*303*#0##,*4*1*#0##,*4*0*#0##and*4*302*#0##to the 99-zone central; its enum names 102 winter, 202 summer and 302 generic protection, so "protection" is 302 (202 would also switch the plant to summer). Encyclopedia: WHO 4what.mdtable. Capture: a MyHomeServer1 plant in summer mode reports*4*0*#0##(MyHOME#453). Not verified: how a physical central unit reacts.test_fix1_central_mode_uses_the_who4_what_table(12 cases; also checks what OWNd's parser reads back: off, heat, cool and auto as themselves, antifreeze 102 and protection 302 asoff),test_fix1_captured_central_unit_mode_parses_as_coolingset_fan_speed: always the plain zone*#4*Z*#11*S##;ValueErrorfor central-unit addresses and for speeds outside 0–3*#4*#1*#11*1##,*#4*#0*#11*1##,*#4*#0#2*#11*1##and*#4*1*#11*4##produce no frame.*#4*1*#11*0..3##and zone 99 do. Encyclopedia: speed is 0 or 1..3.test_fix2_*(4 tests)set_mode(AUTO)is always built as*4*311*#Z##, also for a zone flagged standalone (was*4*311*Z##)*4*311*1##produces no frame;*4*311*#1##and*4*303*1##do. BTicino client:setAutomaticsends*4*311*#23#1##. Encyclopedia: AUTO is*4*311*#WHERE##. An earlier revision returnedNone; MyHOME flags every zone that is not a central unit standalone by default, so that silently dropped AUTO for zones that do run under a central unit (gdluck's review, 2.4).test_fix3_auto_is_always_built_for_the_hash_zone#actuatorfalls back to actuator 1 instead of raisingIndexError*#4*#0*20*0##and*#4*100*20*0##(141 messages). Capture:*#4*1#1*20*1##test_fix4_*(2)ValueError*#4*40*19**0##,*#4*40*19*1*##. Gateway:*#4*41*19*0*0##test_fix5_*(2)_DIMENSION_REQUESTaccepts#param, so*#18*51*511#10#2##and*#18*51*52#26#9##parse*#18*51*511#10#1##and*#18*51*52#26#9##were accepted. Encyclopedia: WHO 18 dimensions 511 and 52. Code: the builders produced requests OWNd could not parse.test_fix6_*(2)set_brightness(0)sends switch-off; the level is capped at 100*#1*31*#1*100*0##and…*100*5##produce no frame;*1*0*31##,*1*0#1*31##…*1*0#255*31##and*#1*31*#1*101*0##do (au1,au4)test_fix7_*(130 cases)interfaceis also read for WHO 0 and WHO 14*0*1*31#4#01##and*14*0*31#4#01##are routed like a light. Encyclopedia (WHO 0):01..99#4#I. WHO 14 rests on the firmware alone.test_fix8_interface_is_read_for_scenarios_and_who14OWNEvent, not "contact OFF"*25*11#121*10##,*25*12*131##. Encyclopedia: dry contact is WHAT 31/32. Capture: real 31/32 and 21–24 frames still parse to the same class (MyHOME#453, #466).test_fix9_*(2)au1,au2.own,au4) and Gateway for every replacement. Capture: WHO 15 MyHOME#541, WHO 25 MyHOME#418.test_fix10_matrix_lists_no_frame_the_firmware_refuses,test_fix10_matrix_examples_all_parseNot included
openservernot emulated; the real gateway's refusal may mean no sound system installed) and gdluck did not propose it. MyHOME skips WHO 16 discovery and themedia_playerplatform when the profile lacks WHO 16, so it would break MyHomeServer1 users with a sound system. master already reports MyHomeServer1 sound as unmeasured. It needs a WHO 16 test on a real gateway with a sound system.Downstream notes
off, so a plant that echoes antifreeze or protection showsoff. That matches Home+Control plants, which report a zone switched off as*4*102*Z##or*4*202*Z##(golden captures, MyHOME#429). Giving them their own mode name would need a matching HVAC mode or preset in MyHOME, so it is left out here. MyHOME never sends antifreeze or protection.*4*311*Z##, which the firmware drops. It now goes out as*4*311*#Z##and reaches the bus.unique_idfor WHO 0 / WHO 14 frames with#4#NN; check MyHOME entity IDs when bumping.Found during review, not in this PR
The golden corpus was checked against the firmware data (gdluck's
au1–au4, audit and real-gateway probe, own-firmware-oracle results, MyHOME traces). The firmware data contradicts none of the 48community-plant-capturefixtures, though it does not cover many of them. Six document-derived fixtures disagree with it; #83 corrects them, together with the three small code findings from the audit (fan zone parsing, capped brightness log, WHO 25 command parser):*4*110*1##"antifreeze" and*4*111*1##"protection": the firmware drops 110 and treats 111 as automatic heating; libqtdevices names them WIN_MANUAL and WIN_AUTOMATIC. The corpus's own captures show antifreeze as*4*102*Z##.*2*1*21#4#1##: a one-digit interface is refused (au2, MH200N oracle); every interface capture uses two digits.*25*31#1*15##: real dry contacts on MH200N, MH202, F454 and H4890 all have a WHERE starting with3.*#4*#0*14##: refused by a real MyHomeServer1 (gateway probe) and by an F454 (MyHOME#629).*15*01#3*0001##: the spec allows WHERE0001, but bt_luci refuses it. No capture exists either way.own-firmware-oracle#11 needs a re-run of the MH200N suites with
bt_termoandbt_energiabefore its WHO 4 / WHO 18 rows are used (the harness keeps only the openserver clients named in the target spec).Other changes
encyclopediaand the firmware run, notlegrand-spec; the three 99-zone ones also cite the BTicino client test that sends the same frame. The golden README requires a corrected fixture to name its new source.sound.py: they now say WHO 16 only. OWNd has no WHO 22 parser (gdluck's README note).Downstream (MyHOME, not in this PR)
climate.pyasync_set_fan_modedid not catch the newValueError(fix 2): OpenWebNet-HA/MyHOME#641 turns it into a service error (merged). OpenWebNet-HA/MyHOME#630 aligns MyHOME's tests and climate code with this PR, including AUTO on standalone zones. Conformance run 37518042220 (OWNd885b8e4+ MyHOMEde8cf4b3) is green. #630 merges once this PR is released as OWNd 2.0.0b10.v2-phase1-architecture, 11 tests still assert the old frames; #630 updates them:test_climate_central_discovery.py,test_component_climate.py(2),test_gateway_messages.py::TestDryContactEvents::test_dry_contact_off,test_golden_conformance.py(4 central-mode builder-parity cases, fromtests/golden/frames/who04_thermo.yaml),test_heating.py::test_set_fan_speed_variations,test_phase2_architecture.py(2).Verification
885b8e4: 1,199 passed, 1 skipped. 3,631 / 3,631 statements covered (scripts/verify_coverage.py).Golden corpus & smoke gatesandMyHOME test suitefail as expected, because they run MyHOME'sv2-phase1-architecture, whose tests still assert the old frames (the 11 tests listed above). Dispatched against MyHOME#630 instead, both jobs pass on885b8e4: run 37518042220.ruff check,tools/golden/validate_corpus.py(115 frames) and the README profile table are clean.bt_termoandbt_energia, so its WHO 4 and WHO 18 NACKs are not evidence about the MH200N; see the correction on the cross-validation comment. Apart from that, fixes 4, 5 and 9 rest on the Emitted corpus and the unit tests, not on a replay.🤖 Generated with Claude Code