Repository navigation
fix(energy): refuse WHO 18 DIMENSION 52 frames outside their tested forms on every send path (four-digit year crashes F460 / F461 / 3488); Wh wording, fixture labels (Encyclopedia audit, group 4) - #157
Conversation
report 21 E8, E12. E8: README (WHO catalog table, generated by scripts/sync_documentation.py) and docs/protocol_conformance_matrix.md said "kWh" for the WHO 18 consumption values. Encyclopedia who-18-energy-management/dimensions.md "DIMENSION 72 - totalizer state" gives Wh and README.md "Measurement model" never uses kWh; the F520 capture *OpenWebNet-HA#18*51*51*23791364## is 23,791,364 Wh. ENCY_CONFIRM EA-E8: kWh is a display conversion, not a wire unit. E12: tests/golden/frames/who18_energy.yaml labelled *OpenWebNet-HA#18*1*1##, *OpenWebNet-HA#18*1*1*1450##, *OpenWebNet-HA#18*1*11## and *OpenWebNet-HA#18*1*11*00452300## as legrand-spec active-power / "45,230.0 kWh" totalizer frames. dimensions.md "Identifier table" has no DIMENSION 1 or 11 (active power is 113, the totalizer 51); zigbee-variant.md "Dimension table" gives 11 as the ZigBee Voltage dimension; addressing.md gives no family for WHERE 1. They stay as `conflict` grammar-only records (OWNd parses them with message_type None); corpus.json regenerated with tools/golden/validate_corpus.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uators report 21 E1, E2, E3, E4, E5, E6, E7, E9, E10, E11. E1: OWNEnergyCommand.report_current_month_energy emits only the documented *OpenWebNet-HA#18*WHERE*52#YY#M*Wh## (dimensions.md "DIMENSION 52 - monthly energy/unit totalizer"; year/month default to the current month). ORACLE_CONFIRM E1: *OpenWebNet-HA#18*51*52*100## refused on 11/12 emulated gateways, the four-digit 52#2026#10 crashes the emulated F460 020012 / F461 020011, the two-digit *OpenWebNet-HA#18*51*52#26#9## goes out on nine (firmware oracle energy-params / energy-ts10). The parser gives monthly_consumption year/month/value and returns None fields (never an empty dict) for a reply without #Y#M. E2: Stop&Go builders accept the published 1N domain, N = 1..127 (addressing.md "Published WHERE forms"; ENCY_CONFIRM EA-E2 functional-only, OWNd narrower than every reading); OWNStopAndGoEvent.line decodes 110..1127. ORACLE_CONFIRM E2: *18*21*110##, *18*21*120##, *18*21*1127## and *18*22*120## forwarded by the same nine gateways as *18*21*11##. E3: set_test_frequency bound 1..180 (dimensions.md "Stop&Go self-test interval"; ORACLE_CONFIRM E3: #212*200 nack/silent 12/12, #212*180 out on 9). E4/E5: decoders for DIMENSION 512#M (tags 1..24 / 25), 71 (six fields), 72#Tot_N, 73 and WHATs 57/58/59/510, 71, 73[#Time], 74, 75#Tot_N (dimensions.md "Energy Management actuator information", "DIMENSION 512 - monthly-average hourly series"; what.md "Historical-series commands", "Actuator commands", "Reset totalizer"); builders for the same, every frame in the firmware oracle energy-ts10 suite (out on eight or nine gateways). E6: DIMENSION 511 tags outside 1..24 / 25 keep message_type None (dimensions.md "DIMENSION 511 - daily hourly series"). E7: report_partial_month_energy / report_partial_day_energy drop the undocumented 53#M / 54#D forms (dimensions.md "DIMENSION 53 / 54"). E9: DIMENSION 1200 docstrings describe Time as the published report-on-change parameter (dimensions.md "DIMENSION 1200"). E10: logs and the new OWNEnergyEvent.address keep the complete WHERE (addressing.md); sensor stays for compatibility. E11: DIMENSION 259 ("Check off/on", dimensions.md "Individual Stop&Go flags") sets the new is_check_active, not is_tracking_active (WHAT 28/29). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… guard (decisions D20, D21, D72) Precedent gate on report 21 E1, E3, E7: - E1: `report_current_month_energy` without `year`/`month` builds the legacy `*OpenWebNet-HA#18*W*52*Wh##` again (deprecated in the docstring) because MyHOME `expose.py` relies on it and `tests/test_expose.py` pins `*OpenWebNet-HA#18*51*52*120500##`; the documented `52#YY#M` form is built when both are given (ORACLE_CONFIRM E1 unchanged). - E7: `report_partial_month_energy(month=)` / `report_partial_day_energy(day=)` keep their optional parameters as deprecated, validated (1..12 / 1..31, bool/fractional/non-finite rejected, D21) and ignored on the wire; MyHOME `expose.py` / `tests/test_expose.py` call them. - E3: `set_test_frequency` rejects bool / fractional days (D21) and is framed as the TS10_1_0_23 touchscreen extension it is (Encyclopedia who-18 dimensions.md "Historical touchscreen extensions"; mcp#24, D72), not as a WHO_18.pdf form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mpatibility (review follow-up) Review follow-up on report 21 E4, E5, E6, E11: - `_require_actuator_where` requires the documented `7N#0` form with N = 1..255 (a bare `7N` gets `#0` appended; `7`, `70`, `701`, `7256` are refused; Encyclopedia who-18 addressing.md "Published WHERE forms"). `_normalize_actuator_where`, shared with the `5N` readers, is unchanged. - `_decode_what` guards the historical-series and force-actuator parameters, `_decode_monthly_average` the 512 month parameter and the DIMENSION 511 decoder the tag / value fields with `isdigit()`: a non-digit or empty value leaves the typed fields None and never raises. - DIMENSION 259 sets `is_check_active` and keeps setting `is_tracking_active` as on master, so existing consumers see no silent None; the CHANGELOG says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…actuator 7N#0 ranges The Stop&Go WHERE 1N (N 1..127) and the energy actuator 7N#0 (N 1..255) are in the official WHO 18 PDF WHERE table, not only the Encyclopedia. Re-attribute both comments to the PDF (primary source), keeping the Encyclopedia as the secondary reference. Comment-only; no behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
e688c3e to
5d4f4d5
Compare
…the send layer *OpenWebNet-HA#18*WHERE*52#2026#10## and the value-field form *OpenWebNet-HA#18*WHERE*52#2026#10*100## crash F460 020012 and F461 020011 after the telegram goes out (findings/audit-crash-frames.md; crash_frames ledger rows); the documented two-digit form *OpenWebNet-HA#18*WHERE*52#26#9## does not. No builder emits the four-digit form, so the exposure is a raw frame handed to a session. Every send path now refuses it the way it refuses the WHO 4 dimension 22 crash forms: a new is_who18_dimension_52_four_digit_year() detector in energy.py (exported), the refusal helper and its error take a reason string, and both send entry points check the new detector after the dimension 22 one. OWNCommand.parse stays permissive (decision D1). Tests: detector cases, both session entry points refusing before any connect, and the two-digit form left alone; full suite 2923 passed, mypy clean. Found by the 2026-11-04 audit (E18-1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
One more commit here from the 2026-11-04 cross-check, on this PR's WHO 18 ground: a DIMENSION 52 request carrying a four-digit year ( |
|
Live on the F461 2.0.11 test plant (2026-11-04):
|
The WHO 18 PDF labels DIMENSION 51..54 'Watt' (p.19-20, p.25), 511/512 'Watt/h' (p.6, p.18) and only 72 'Wh' (p.21); kWh appears nowhere, and the Encyclopedia README says to preserve the published field semantics. The README and conformance-matrix rows keep '(Wh)' as what OWNd and MyHOME read, say so, and the golden corpus header no longer claims the wire values 'are Wh'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks for auditing WHO 18 against the Encyclopedia and firmware oracle — especially catching the 4-digit-year DIMENSION 52 daemon crash on F460 / F461 and cleaning up the grammar-only golden fixture notes without deleting them.
Applying our standing rule — if we don't have real, committed on-wire plant traces from physical hardware, we don't change behaviour — please trim this PR down to the proven crash guard and documentation/fixture-label fixes.
What can stay
- 4-digit-year
DIMENSION 52send-layer crash guard (is_who18_dimension_52_four_digit_year): Refusing*#18*WHERE*52#YYYY#M...##withreason="destructive_frame"in the send paths, matching the existingWHO 4DIMENSION 22crash guard (F460 020012/F461 020011crash confirmed in the firmware oracle). - Documentation & golden
conflictrelabelling: ClarifyingWhvs. the PDF's"Watt"label inREADME.md,docs/protocol_conformance_matrix.md,scripts/sync_documentation.py, andupdate_interval/stream_durationdocstrings, plus relabelling the four*#18*1*1##/*#18*1*11##records intests/golden/frames/who18_energy.yaml(corpus.json) tosource: conflictwith accuratemcp_noteswhile keeping parser roundtrip coverage intact.
Blockers — Behaviour changes without committed plant traces (please revert)
| # | Symbol / File | Behaviour change without real plant trace | Required action |
|---|---|---|---|
| 1 | OWNEnergyCommand.report_partial_month_energy & report_partial_day_energy in OWNd/message/energy.py |
Silently drops #M and #D on the wire when callers pass month= / day= (*#18*51*53#10*300## -> *#18*51*53*300##, *#18*51*54#18*120## -> *#18*51*54*120##), deleting existing assertions in tests/test_message_protocol.py. |
Revert both builders to master so passing month / day preserves existing wire output; document the WHO_18.pdf scalar vs. TS10 parameterised distinction in docstrings only. |
| 2 | OWNEnergyEvent human_readable_log, DIMENSION 52 monthly_consumption, & DIMENSION 511 tag bounds in OWNd/message/energy.py |
Rewrites human_readable_log across every OWNEnergyEvent from Sensor {sensor} to Sensor {address}, changes monthly_consumption dict schema on parameter-less *#18*51*52*12345## (date/year/month keys), and drops DIMENSION 511 tags outside 1..25 — all without new physical meter traces. |
Revert existing OWNEnergyEvent log strings, DIMENSION 52 dict shape, and DIMENSION 511 behaviour to master. |
| 3 | 10 new OWNEnergyCommand builders & 9 new OWNEnergyEvent decoders (DIMENSION 512, 71, 72, 73, WHAT 57..510, 71..75, is_check_active) in OWNd/message/energy.py |
Adds historical series and 7N#0 actuator builders/decoders (including DIMENSION 72 reset timestamp with an explicitly "inferred" 2000 + Y year width) backed only by WHO_18.pdf (ownkb:claim:c002261, c002262) and empty-bus gateway forwarding, with zero physical energy-meter or 7N#0 load-actuator bus traces. |
Hold back all new speculative builders and event decoders until we have real hardware captures for them. |
| 4 | OWNStopAndGoCommand / OWNStopAndGoEvent (11..1127 addressing & set_test_frequency 1..180) in OWNd/message/energy.py |
Widens Stop&Go addressing from 11..19 to 11..1127 (with overlapping bare-line vs. 1N normalisation where 11 means line 1 while 10 means line 10 / 110) and narrows set_test_frequency from 1..255 to 1..180 based on TS10_1_0_23 (libqtdevices) + empty-bus oracle forwarding, with zero physical Stop&Go breaker traces. |
Revert Stop&Go addressing and set_test_frequency range validation to master; note the WHO_18.pdf 1..127 and TS10 1..180 domains in docstrings only. |
… the dimension 52 crash guard and docs Maintainer review of OpenWebNet-HA#157: without committed on-wire plant traces from physical hardware, runtime behaviour is not changed. Spec PDFs, Encyclopedia prose, archived touchscreen client code and emulated-gateway verdicts are documentation context only; the oracle crash finding justifies a crash-prevention guard. Kept: - the DIMENSION 52 four-digit-year send-layer guard (`is_who18_dimension_52_four_digit_year`, `reason="destructive_frame"` in OWNd/connection/command_session.py) and tests/test_who18_dimension52_guard.py; - the Wh-vs-'Watt' wording in README.md, docs/protocol_conformance_matrix.md and scripts/sync_documentation.py, the update_interval / stream_duration / start_sending_instant_power docstrings; - the four `*OpenWebNet-HA#18*1*1##` / `*OpenWebNet-HA#18*1*11##` golden records relabelled `source: conflict` with grammar-only mcp_notes (roundtrip coverage kept). Blocker 1 (report_partial_month_energy / report_partial_day_energy): both builders reverted to master, so `month=` / `day=` still emit `*OpenWebNet-HA#18*51*53#10*300##` / `*OpenWebNet-HA#18*51*54#18*120##`; master's assertions in tests/test_message_protocol.py restored; the WHO_18.pdf scalar vs TS10 parameterised distinction is noted in the docstrings only. `report_current_month_energy` is master's code (its `52#YY#M` two-digit-year form and the parameter-less form predate this PR and are unchanged); its docstring now documents the form and points at the crash guard. Blocker 2 (OWNEnergyEvent): reverted to master in full: the human_readable_log wording ("Sensor {sensor}"), the DIMENSION 52 `monthly_consumption` dict shape on `*OpenWebNet-HA#18*51*52*12345##`, the DIMENSION 511 tag-bounds change, and the `address` property and `_decode_what` / `_decode_dimension` restructuring that carried them. tests/test_who18_auto_update_reply.py restored to master. Blocker 3 (new builders and decoders): removed the ten OWNEnergyCommand builders (start_daily_series, start_monthly_average_series, get_monthly_average_consumption, enable_actuator, force_actuator, end_actuator_forcing, reset_totalizer, request_actuator_status, request_totalizer, request_differential_current_level), the nine OWNEnergyEvent decoders (DIMENSION 512, 71, 72, 73; WHAT 57..510, 71, 73, 74, 75), `OWNStopAndGoEvent.is_check_active`, the `_require_actuator_where` / `_validate_int_range` / `stop_and_go_line` helpers, the new constants and their exports from OWNd.message, and their tests. tests/test_who18_audit_fixes.py keeps only the E1 builder-output pins (unchanged master output), E8, E9 and E12. Blocker 4 (Stop&Go): `_normalize_stop_and_go_where` and `OWNStopAndGoEvent.line` reverted to master's 11..19 addressing and normalisation; `set_test_frequency` back to master's 1..255 without the bool / fractional check; the WHO_18.pdf 1..127 and TS10 1..180 domains are noted in docstrings only. tests/test_who18_stop_and_go.py restored to master. CHANGELOG: entries for the reverted items removed; the kept entries are reworded as documentation / fixture relabelling with no behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…plant traces; keep the WHERE guards and docs Maintainer review of OpenWebNet-HA#159: without real, committed on-wire plant traces from physical hardware, runtime behaviour does not change. Spec PDFs, Encyclopedia prose and empty-bus gateway ACKs / echoes are documentation context; oracle NACKs justify builder guards only. Blocker 1 (OWNAutomationEvent WHAT 10/11/12, 13/14, DIMENSION 11): reverted to master. `state` again carries any command WHAT (10/11/12 included), WHAT 13/14 keep `state = 13/14` with `is_opening` / `is_closing`; no `unknown_command_code`. Removed the `step`, `moves_to_end_point`, `priority`, `priority_bitfield`, `priority_safety/high/medium`, `selector`, `info_name`, `is_command_not_executed`, `target_level`, `is_go_to_level` decoders on the event and on `OWNAutomationCommand`, the related log strings, the module-level constants and helpers, and their tests. Kept: the module docstring with the WHO 2 specification caveats, including the frame-shape note (F461 2.0.11 echoes the DIMENSION 11 write form `*michnovka#2*25*OpenWebNet-HA#11#001#1*50##`; the PDF publishes `*michnovka#2*WHERE*OpenWebNet-HA#11#PRIORITY#LEVEL*SELECTOR##`). Blocker 2 (report_shutter_level / DIMENSION 10): reverted to master. `state` 10..15 and `priority` / `info` 0..255 are accepted again, the level-255 log string is master's, DIMENSION 10 `state` outside 10..14 is decoded as before; `tests/test_message_protocol.py` restored from master. Blocker 3 (golden `cover.dim.write.pos.75`): restored, not deleted, relabelled `source: conflict` with `mcp_notes` explaining that the PDF / Encyclopedia define no DIMENSION 10 write (the position write is DIMENSION 11) and that no committed plant trace carries the frame, so it pins parser shape only (the pattern OpenWebNet-HA#157 used for who18_energy.yaml). corpus.json regenerated from the YAML files (no repository tool exists; same json.dumps(indent=2) layout as the committed file). The `cover.dim.write.tilt.30` -> `cover.dim.write.level.30.no_priority` relabel is kept (documentation). Blocker 4 (WHO 3): `_LOAD_GENERAL_STATUS` in base.py, the `OWNCommand.parse` pre-screen change and `OWNLoadCommand.general_status()` reverted to master with their tests; `*michnovka#3*##` is rejected as before. Blocker 5 (WHO 9): the runtime `(WHAT N, unverified name)` log suffix, `state_name` and `is_state_name_verified` reverted; master's log branches restored verbatim. Kept: the provenance caveat as comments on `AUX_WHAT_NAMES` / `AUX_WHAT_NAMES_UNVERIFIED` and the `OWNAuxEvent` docstring. Kept: the non-empty-WHERE guards on `status`, `get_shutter_status`, `raise_shutter`, `lower_shutter`, `stop_shutter` (`*2*1*##` and `*michnovka#2**10##` NACKed 12/12 in the firmware oracle; `OWNCommand.parse` stays permissive, pinned by a test) and the "Slat Tilt" / "tilt angles" removal in README.md, docs/protocol_conformance_matrix.md, scripts/sync_documentation.py and tests/golden/README.md. The generated WHO 2 catalog description no longer claims advanced-command decoding. CHANGELOG entries for the reverted items removed; the kept ones say no behaviour change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Applied in
Kept, as listed under "What can stay": the Committed plant evidence for the held-back decoders. Physical frames for three of them are already committed in MyHOME: the |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks @gdluck for cleanly reverting all four runtime blockers from #5769893949 in 37474c5..d88eed3 — the WHO 18 Encyclopedia headings ("DIMENSION 1200 - automatic active-power updates", "Measurement families and automatic updates", "Identifier table", "DIMENSION 72 - totalizer state") and golden conflict relabelling on *#18*1*1## / *#18*1*11*00452300## are well grounded.
As on #155 and #156, we ran our Round 2 / @fedem95-style verification pass so this can merge cleanly once green. Five blocking items and a few Round 2 hygiene items came up (plus a rebase onto master now that #155 and #156 have landed), followed by a direct answer to your question in #6633945785 at the bottom:
1. AsyncSerialTransport.send is unguarded (breaks the 4-send-path contract) + guard test gaps
CHANGELOG.md,OWNd/message/energy.py:341, and commit37474c5state that*#18*WHERE*52#2026#10##is refused "by every send path withreason=\"destructive_frame\", like the WHO 4 dimension 22 crash forms", yetOWNd/transport/serial.py:149-179is untouched.- Because
*#18*51*52#2026#10##parses as a validOWNEnergyCommand(is_valid is True), callingawait serial_transport.send("*#18*51*52#2026#10##")(or without##, sinceserial.py:164appends##before checkingis_who4_dimension_22_crash_form) writes the crash frame straight to the serial port. Please wire the WHO 18 Dimension 52 guard intoAsyncSerialTransport.sendalongsideis_who4_dimension_22_crash_form(or share a common destructive-frame classifier betweencommand_session.pyandserial.pyso they cannot drift). - Test gaps in
tests/test_who18_dimension52_guard.py:test_two_digit_year_is_not_refused_by_the_guard(lines 74-80) createssession = OWNCommandSession(gateway=_gateway())and never callsawait session.send(...)— assertingsession._last_error is Noneon a fresh instance is a no-op.- Please bring
test_who18_dimension52_guard.pyto parity withtests/test_who4_dimension22_guard.py: actually send safe forms (*#18*51*52#26#9##,*#18*51*52#26#9*100##,*#18*51*52##,*#18*51*52*100##) through mockedOWNCommandSession.send,send_to_gateway,AsyncTcpTransport.send, andAsyncSerialTransport.sendassertingwriter.written == [frame.encode()]; assert refusal across all 4 entry points (includingAsyncSerialTransport.sendwith/without##,connect.assert_not_awaited()on disconnectedsend_to_gateway, and single serialization via_CountingCommand); and cover the new public export intests/typing/(test_typing_consumer.py).
2. Wire-escape bypasses in is_who18_dimension_52_four_digit_year (atoi(), grammar, and buffer/chunk forms)
In OpenWebNet-HA/own-firmware-oracle (findings/audit-crash-frames.md), firmware packs Y from 52#Y#M via C atoi() into a 1-byte hex slot (26 -> 1A, 9 -> 09 in `$06D1A1023350161A09\r`), whereas 2026 (0x7EA) overflows that 1-byte formatter into `$006D1A1023350167EA0\r`.
With _WHO18_DIM52_FOUR_DIGIT_YEAR = re.compile(r"^\*#18\*[^*]+\*52#[0-9]{4}#[0-9]{1,2}(?:\*[^*]*)?##$", re.ASCII) and str(frame), all of the following frames return False from is_who18_dimension_52_four_digit_year and parse as is_valid is True in OWNMessage.parse (OWNd/message/base.py:99-115), sailing past both checks onto the wire:
- 5-digit leading-zero year (
atoi("02026") == 2026) or 3-digit overflow year (>= 256):*#18*51*52#02026#10##([0-9]{4}misses 5 chars, while Catoi("02026") == 2026emits the exact same`$006D1A1023350167EA0\r`crash telegram) and*#18*51*52#256#10##/*#18*51*52#999#10##(WHO_18.pdfdefinesYstrictly as two-digit00..99). - Leading zeros on
WHOorDIMENSION(atoi("018") == 18,atoi("052") == 52):*#018*51*52#2026#10##and*#18*51*052#2026#10##(base.pyuses\d+andint("018") == 18). - 3+-digit leading-zero month (
atoi("010") == 10):*#18*51*52#2026#010##([0-9]{1,2}rejects010;base.pyaccepts(?:#\d+)*). - Multi-value dimension reply (
(?:\*\d*)+in_DIMENSION_REQUEST_REPLY):*#18*51*52#2026#10*100*0##((?:\*[^*]*)?matches at most one*token;base.py:109accepts one or more). - Dimension-write
#52(_DIMENSION_WRITINGinbase.py:113):*#18*51*#52#2026#10*100##. - Extra
#parameter ((?:#\d+)*inbase.py:101):*#18*51*52#2026#10#1##. - Empty
WHERE((?P<where>#?\d+)?is optional inbase.py:100):*#18**52#2026#10##([^*]+requires non-emptyWHERE;base.pyparses it as validOWNEnergyCommand). - Non-ASCII Unicode digits (
base.py\d+has nore.ASCII):*#18*51*52#2026#10##(int("2026") == 2026inbase.py, while the guard'sre.ASCIIreturnsFalse). - Public helper buffer / chunk / anchor contract (compare
is_who4_dimension_22/is_who4_dimension_22_crash_forminOWNd/message/heating.py:109-175):b"*#18*51*52#2026#10##"/bytearray/memoryviewcurrently pass throughstr(b"...")->"b'...'"->False(decode(bytes, bytearray, memoryview)vialatin-1); concatenated frames"*1*1*11##*#18*51*52#2026#10##"returnFalseinstead of inspecting each##-delimited chunk; and use\Zrather than$(which matches before trailing\n).
Please follow heating.py's two-step pattern ("the allowance is the tested forms, not the complement of the crash set"): detect any WHO 18 Dimension 52 chunk (^[^*]*\*#\s*\+?0*18\s*\*[^*]*\*\s*#?\s*\+?0*52(?:\s*\Z|[#*\s]) per ## chunk after latin-1 decode), and allow only the canonical safe forms (re.ASCII, \Z-anchored, non-empty canonical ASCII WHERE): parameter-less *#18*<where>*52\Z / *#18*<where>*52*<digits>\Z, and canonical two-digit-year *#18*<where>*52#<YY>#<M>(?:\*<digits>)?\Z with ASCII <YY> in 00..99 ([0-9]{1,2}) and <M> in 1..12 (0?[1-9]|1[0-2]). (Naming the detector is_who18_dimension_52_crash_form — keeping is_who18_dimension_52_four_digit_year as an alias if desired — also matches is_who4_dimension_22_crash_form now that 256 / 02026 / #52 are covered.)
3. Firmware-oracle provenance & missing 3488 (030205, HOMETOUCH) crash attribution
DESTRUCTIVE_DIM52_REASON,energy.py, andCHANGELOG.mdlist onlyF460(020012) andF461(020011). InOpenWebNet-HA/own-firmware-oracle(findings/audit-crash-frames.mdandtools/expected_divergences.yaml, merged inown-firmware-oracle#76/#77),*#18*51*52#2026#10##and*#18*51*52#2026#10*100##also crash3488(030205, HOMETOUCH) (bus=[$006D1A1023350167EA0],verdict=crash). Please include3488(030205) alongsideF460(020012) andF461(020011).energy.pycites bare paths (findings/audit-crash-frames.md,tools/expected_divergences.yaml) whiletests/golden/firmware_oracle.jsondoes not yet containownd-audit-frames. Either syncfirmware_oracle.jsonor explicitly cite theOpenWebNet-HA/own-firmware-oraclerepository, suiteownd-audit-frames, its index SHA-256 (db03cc878d84d514b9e324c2d01d050264f2e4ccd0c1aad33937683c30e472ca), and theemulated firmware, harness=full, not yet replayed on physical hardwarecaveat as inheating.py:90, 120.
4. Leftover reverted doc claim, unbacked Stop & Go trace claim, and minor citation nits
README.md:92&scripts/sync_documentation.py:190: Both still append", Energy Management actuators"to theWHO 18catalog row, left over from the reverted Round 17N#0actuator commands/events. Please remove", Energy Management actuators"in both files (and keepREADME.md's top-level WHO catalog cell concise —Active power (W), hourly/daily/monthly consumption (Wh), Stop & Go breaker diagnostics— leaving the parenthetical PDF'Watt'vs'Wh'errata detail indocs/protocol_conformance_matrix.mdandenergy.py).- Stop & Go docstrings (
OWNd/message/energy.py:698-700and726-727): Both claim11..19/1..9is "the range covered by committed plant traces". There are zero committed physical Stop & Go (1N) device reply/event traces inOWNdorMyHOME(OWNStopAndGoEvent's own docstring at line 650 correctly notesDerived from firmware evidence (energy-ts10 suite...)). Please reword to state accurately that11..19(line1..9) ismaster's existing range fromenergy-ts10, kept unchanged until a physical Stop & Go trace is committed. - Minor docstring/citation nits:
OWNd/connection/command_session.py:278:_refuse_destructive_frame's docstring still says"Record and log the refusal of a crashing WHO 4 dimension 22 frame, never written."— update to matchdestructive_frame_errorat line 53 ("frame known to crash a gateway").tests/golden/frames/who18_energy.yaml:5&tests/test_who18_audit_fixes.py:114:zigbee-variant.mdheading is## `DIMENSION` table(uppercaseDIMENSION).OWNd/message/energy.py:609: the committedMyHOME#466sweep (myhome_sweep_F454_all_2026-09-26T16-59-13.json:28) records"firmware": "2.0.51"rather than2.0.5.
5. Rebase onto master, CHANGELOG.md section, private audit IDs, and PR Title / Body
- Rebase:
#155(13b1a8a) and#156(32cca9e) have merged tomaster; please rebase and regeneratetests/golden/corpus.json. CHANGELOG.md& private audit tags: Move theDIMENSION 52crash guard from### Changedto### Fixed(matchingWHO 1,WHO 4, and[2.0.0b13]), and strip internal audit tags ((2026-11-04 audit E18-1),report 21,E1..E12,decision D1) acrossCHANGELOG.md,tests/golden/frames/who18_energy.yaml:10,tests/test_who18_audit_fixes.py(docstring +test_e1_*..test_e12_*names), andtests/test_who18_dimension52_guard.py:21.- PR Title & Body: Please update both the PR title and the PR description sections (
## Summary,## Changes,## State Diagram) so they describe only what ships in this PR rather than the reverted Round 1 features.
Re: Follow-up question (#6633945785) on 7N#0, DIMENSION 511, and DIMENSION 512
Please wait until we have real physical device reply captures before opening a decoder PR for those three:
DIMENSION 511&512inf461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json: Asf461_live_2026-10-23/README.md(lines 25, 57, 93) documents, that bench had noWHO 18meters (Not present: ... WHO 18 meters). Lines2416-2494(*#18*51*511#10#1##,*#18*51*511#10#2##,*#18*51*512#10##) are outbound clienttxprobes fromrun1_oracle_replay_readonly(replies: []), not meter replies (*#18*WHERE*511#M#D*Tag*Val##/*#18*WHERE*512#M*Tag*Val##) — so they carry no hardware evidence for reply field order,1..24vs0..23tag indexing,Tag 25totals, or scaling (andOWNEnergyEventonmasteralready decodes511,513,514).7N#0inissue_466&mh202_sound_intercom_ducking: Those captures contain onlyDIMENSION 113active-power replies (*#18*73#0*113*0##..*#18*79#0*113*0##,*#18*71#0*113*99##) and51/1200queries — whichmasteralready decodes (is_actuator == True,target_address == "77#0",sensor == "77",active_power, pinned atwho18_energy.yaml:138). None of them contain aWHAT 71/73/74/75frame or aDIMENSION 71,DIMENSION 72(Tot_N*Energy*D*M*Y*H*m), orDIMENSION 73device reply.
Once a physical trace with actual replies from an F520 / F522 / F523 / 3522 meter or load actuator is committed, a focused PR adding those decoders will be very welcome!
…ilders # Conflicts: # CHANGELOG.md
…dimension 52 guard allows only the tested forms; 3488 and oracle provenance; wording Review 5819194700 on OpenWebNet-HA#157, five items (the merge with master, OpenWebNet-HA#155 and OpenWebNet-HA#156, is the commit before): 1. AsyncSerialTransport.send applied only the WHO 4 dimension 22 guard, so `*OpenWebNet-HA#18*51*52#2026#10##` (with or without `##`) was written to the serial port. command_session.destructive_frame_reason() is now the one classifier behind OWNCommandSession.send, send_to_gateway and AsyncSerialTransport.send (AsyncTcpTransport.send goes through the session), returning the reason or None. tests/test_who18_dimension52_guard.py mirrors tests/test_who4_dimension22_guard.py: safe forms written through all four entry points, refusals across all four (serial with and without `##`), connect.assert_not_awaited() for the session and the one-shot helper, single serialisation, buffers and concatenated chunks; the test that never sent is gone. tests/typing/consumer_send.py exercises both guards. 2. The regex `52#[0-9]{4}#` was the complement of two crash rows, not the tested forms: `52#02026#10` and `52#256#10` reach the same one-byte atoi() formatter, and `*OpenWebNet-HA#18*`, `*052`, `OpenWebNet-HA#10`, `*OpenWebNet-HA#52` writes, extra `#` parameters, an empty WHERE, fullwidth digits and multi-value replies all parsed as valid and passed the guard. is_who18_dimension_52 / is_who18_dimension_52_crash_form follow heating.py's two-step pattern (detect any DIMENSION 52 chunk after latin-1 decoding; allow only `*OpenWebNet-HA#18*W*52`, `*OpenWebNet-HA#18*W*52*Wh`, `*OpenWebNet-HA#18*W*52#YY#M`, `*OpenWebNet-HA#18*W*52#YY#M*Wh` with ASCII YY 0..99 and M 1..12, `\Z` anchors). is_who18_dimension_52_four_digit_year stays as an alias. 3. The 3488 030205 (HOMETOUCH) crash rows are cited with the F460 and F461 ones, with the oracle provenance (repository, suite ownd-audit-frames, index SHA-256, emulated firmware caveat), in energy.py, the reason string and CHANGELOG. 4. README and scripts/sync_documentation.py no longer list "Energy Management actuators"; the Stop&Go docstrings say 11..19 is master's range from the oracle energy-ts10 suite (no physical Stop&Go trace is committed); _refuse_destructive_frame's docstring, the zigbee-variant.md heading ("DIMENSION table") and the F454 firmware (2.0.51) are corrected. 5. Audit tags (report 21, E1..E12, decision D1) removed from the tests and golden notes; the CHANGELOG entry sits under Fixed (merge commit); PR title and body are rewritten on GitHub. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks. All five are in c208317, on top of a merge with
On Run here (Python 3.14, Generated by Claude Code |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Code Review & Audit Summary: APPROVED
Comprehensive audit and local verification of #157 completed against commit c208317 (fix/who18-energy-builders).
1. Protocol & Firmware Vulnerability Protection
- Root Cause Verified: Firmware replay in own-firmware-oracle (
ownd-audit-framessuite) confirms that monthly energy requests carrying four-digit years (such as*#18*51*52#2026#10##and value form*#18*51*52#2026#10*100##) crash the command translator daemons on F460020012, F461020011, and 3488030205(HOMETOUCH) due to an 8-bit buffer/slot overflow when formatting theatoi()result into the bus telegram. - Robust Allowlist Defense: Using the dual-regex pattern (
_WHO18_DIMENSION_52_REGEXto detect any DIMENSION 52 frame, followed by_WHO18_DIMENSION_52_SENDABLE_REGEXstrictly allowlisting tested canonical forms) cleanly blocks all 9 bypass classes (leading zeros, three/four-digit years, wide months, multi-value frames, writes, and fullwidth digits) while preserving permissive parsing for bus capture/diagnostics. - Unified Transmission Invariant: Introducing
destructive_frame_reason()ensures all four send paths (OWNCommandSession.send,send_to_gateway,AsyncTcpTransport.send, andAsyncSerialTransport.send) share the identical classifier and error contract (reason="destructive_frame"). Serial transport previously allowed crashing WHO 18 frames through; this gap is now closed. - Pre-connection Fast-Fail:
send_to_gateway()andsend()validate before connecting, preventing socket creation, TLS handshakes, or slot exhaustion on single-connection gateways (MH200N, F453AV). - Single Serialization: Verified that strings checked by the guard are identical to the wire bytes dispatched, avoiding dual-serialization drift.
2. Documentation & Golden Corpus Accuracy
-
Energy Units (Wh): Correctly updates README, conformance matrix, and docstrings to state energy totalizers are Wh. Legrand's specification PDF erroneously labeled DIMENSION 51..54 as 'Watt' (and only 72 as 'Wh'); physical traces (
*#18*51*51*23791364##) confirm cumulative Wh. -
DIMENSION 1200: Clarified that
Timeis the published report-on-change interval in minutes rather than an unconditional polling duration. -
Stop&Go Addressing: Documented master's 11..19 (
$N=1..9$ ) domain alongside the PDF's$N=1..127$ specification, citing TS10 oracle provenance pending hardware traces. -
Golden Fixtures: Relabelled
energy.req.active_power.meter1,energy.resp.active_power.meter1.1450,energy.req.lifetime.meter1, andenergy.resp.lifetime.meter1.45230tosource: conflict(grammar-only). SCS WHO 18 defines no DIMENSION 1 (active power is 113) or DIMENSION 11 (ZigBee voltage).
3. Verification & Test Results
- Full Test Suite: 3,177 passed in 30.2s (
test_who18_dimension52_guard.pyandtest_who18_audit_fixes.pypass 94/94 items). - Coverage: 100.0% statement and branch coverage maintained across
OWNd/message/energy.py,OWNd/connection/command_session.py, andOWNd/transport/serial.py(7,085 / 7,085 statements covered across all 28 modules). - Static Analysis & Decoupling:
ruff check,mypy OWNd tests/typing,verify_library_standards.py, andsync_documentation.py --checkall pass clean with zero errors. - Golden Corpus:
validate_corpus.pyreports all 166 fixtures valid and synchronized. - Downstream MyHOME Compatibility: All 86 energy, sensor, expose, service, and dimension-guard tests in Home Assistant
MyHOMEexecute cleanly against this branch with zero regressions.
cc @fedem95 for review. Ready to merge!
Requested by A
Summary
WHO 18 checked against the Encyclopedia functional pages (
who-18-energy-management/{addressing,dimensions,zigbee-variant}.md) and the own-firmware-oracleownd-audit-framessuite. What ships: a send-layer guard that refuses WHO 18 DIMENSION 52 frames outside their tested forms on every send path (a four-digit year crashes three emulated gateways), and documentation and golden-fixture corrections. No builder output, decoded value or log line differs frommaster; the actuator, series and Stop&Go decoder changes of the audit were held back pending committed plant traces.Changes
OWNd/message/energy.py,OWNd/connection/command_session.py,OWNd/transport/serial.py):is_who18_dimension_52_crash_form(aliasis_who18_dimension_52_four_digit_year) follows the WHO 4 dimension 22 pattern: it detects any DIMENSION 52 chunk (per##chunk,bytes/bytearray/memoryviewdecoded as latin-1) and allows only the tested forms*#18*W*52##,*#18*W*52*Wh##,*#18*W*52#YY#M##,*#18*W*52#YY#M*Wh##(ASCIIYY0..99,M1..12).*#18*51*52#2026#10##and*#18*51*52#2026#10*100##crash the F460020012, F461020011and 3488030205after the telegram went out (OpenWebNet-HA/own-firmware-oracle, suiteownd-audit-frames, index SHA-256db03cc878d84d514b9e324c2d01d050264f2e4ccd0c1aad33937683c30e472ca,findings/audit-crash-frames.md; emulated firmware,harness=full, not yet replayed on physical hardware); the firmware reads the year withatoi()into one byte, so02026,256, leading zeros on WHO / DIMENSION, a#52write, extra parameters, an empty WHERE, non-ASCII digits and multi-value forms are refused too. One classifier,destructive_frame_reason, now servesOWNCommandSession.send,send_to_gateway,AsyncTcpTransport.sendandAsyncSerialTransport.sendwithreason="destructive_frame"; the serial transport previously applied only the WHO 4 guard.OWNCommand.parsestays permissive.docs/protocol_conformance_matrix.md: WHO 18 totalizers are Wh as OWNd and MyHOME read them (the PDF labels DIMENSION 51..54 'Watt' and only 72 'Wh'); the WHO 18 catalog row no longer lists actuators.energy.pydocstrings: DIMENSION 1200Timeis the published report-on-change parameter;report_current_month_energy/report_partial_month_energy/report_partial_day_energynote the WHO_18.pdf forms next to the unchanged wire output; the Stop&Go range 11..19 is master's range from the oracleenergy-ts10suite (no physical Stop&Go trace is committed);set_test_frequencynotes the TS10 domain.*#18*1*1##/*#18*1*11*00452300##relabelledconflictgrammar-only records (no DIMENSION 1 on SCS WHO 18, DIMENSION 11 is the ZigBee variant's Voltage, no kWh on the wire). Frames unchanged.Tests:
tests/test_who18_dimension52_guard.py(detector, bypass forms, buffers and chunks, all four send paths, single serialisation),tests/test_who18_audit_fixes.py,tests/typing/consumer_send.py(both guards). Full suite,ruff check,mypy OWNd tests/typing,scripts/verify_coverage.py(100 % statements and branches on the three touched modules),scripts/sync_documentation.py --check,scripts/verify_library_standards.pyandtools/golden/validate_corpus.pypass. Branch merged withmasterafter #155 and #156.🤖 Generated with Claude Code