Skip to content

fix(automation): WHO 2 builders validate WHERE with require_address_where, ASCII-only priority string; WHO 2 / WHO 9 documentation and golden-corpus errata (Encyclopedia audit, group 2) - #159

Open
gdluck wants to merge 12 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/who2-advanced-whats

Conversation

@gdluck

@gdluck gdluck commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A

Summary

Review round 2 (GreenGrassBlueOcean, 2026-11-09) applied in 4cdf1a8, a merge of master 32cca9e into the branch whose tree is byte-identical to the single rebased commit 9dc1e45 (a history rewrite was not possible from the session that pushed; a squash merge gives the same result, and the rebased commit can be force-pushed instead on request). git diff master is the same either way. The runtime change is argument validation on the WHO 2 builders plus one wire-hygiene fix; everything else is documentation and golden-corpus metadata. No decoded value, log line of a valid frame or accepted parser domain differs from master.

  • OWNAutomationCommand: all seven builders (status, get_shutter_status, raise_shutter, lower_shutter, stop_shutter, set_shutter_level, report_shutter_level) pass their WHERE through master's require_address_where (fix(who1): crash guards for empty DIMENSION values, non-empty WHERE on WHO 1 / WHO 17 builders, DIMENSION 4 status_level / status_speed #155), as the WHO 1 builders do. A WHERE that is not a str / int, a bool, None, empty, blank, or containing *, ## or inner whitespace raises ValueError (leading / trailing whitespace is stripped). Before: raise_shutter(True) built *2*1*True## (log "Raising shutter None."), status(None) *#2*None##, raise_shutter("31##") *2*1*31####, raise_shutter("") *2*1*##, raise_shutter("*") the valid-looking *2*1**##, get_shutter_status("") the valid-looking *#2**10##, status("31*10") the DIMENSION 10 read *#2*31*10##. OWNCommand.parse stays permissive (*2*1*## still parses as an invalid OWNAutomationCommand).
  • report_shutter_level: a string priority is kept verbatim ("001") only when it is ASCII digits. "001" passed str.isdigit() and reached the wire as *#2*31*10*10*40*001*0##; it is now encoded through int (*1*). OWNAutomationEvent is untouched (zero decode diff, as discussed in the review).
  • WHO 2 documentation: the module docstring of OWNd/message/automation.py describes the WHO 2 advanced-command, DIMENSION 10 / 11 and WHERE grammar against the committed MyHOME captures: the LN4660M2 centralised control of a MyHomeServer1 plant putting *2*11#100#001#1*0## / *2*12#100#001#1*0## / *2*10#001#1*0## on the bus (issue_445); an F454 echoing Home Assistant's relative steps *2*11#STEP#001*31## / *2*12#STEP#001*31## (STEP 1, 2, 10) as *2*1000#11#STEP#001#1*31## / *2*1000#12#STEP#001#1*31## while the actuator reports *2*1*31## / *2*2*31##, DIMENSION 10 and its own *2*0*31## (issue_466, config_entry-myhome_F454_step_commands.json); an F461 2.0.11 ACKing *2*13*05## / *2*14*05## and translating them as *2*1000#13#001#1*05## / *2*1000#14#001#1*05## while the F411/2 at 05 emits nothing for any of the five advanced commands (f461_live_2026-11-04, test(f461): live F461 2.0.11 full debug log and bus sweep, 2026-11-04 (released OWNd 2.0.0b13) MyHOME#865). The un-replayed "NACKed by all twelve emulated gateways" claim is gone from the code, the tests and the CHANGELOG: neither *2*1*## nor *#2**10## is in own-firmware-oracle, and the guards are presented as argument validation only.
  • WHO 9: AUX_WHAT_NAMES / AUX_WHAT_NAMES_UNVERIFIED (unused) are removed; the provenance caveat on the WHAT 2..10 names lives in the OWNAuxEvent and channel docstrings only. Log strings unchanged.
  • Golden corpus: cover.dim.write.pos.75 (*#2*93*#10*75##) relabelled source: conflict (WHO_2.pdf / Encyclopedia who-2-automation/dimensions.md define no DIMENSION 10 write; no committed trace carries the frame; it pins parser shape only); cover.dim.write.tilt.30 renamed cover.dim.write.level.30.no_priority (DIMENSION 11 is absolute position; WHO 2 has no slat-angle dimension). README, docs/protocol_conformance_matrix.md, scripts/sync_documentation.py and tests/golden/README.md no longer say "tilt angles" / "Slat Tilt"; the golden README count is the actual 166 (tools/golden/validate_corpus.py: 11 files, 166 frames, PASS).

Review follow-up (round 2, 2026-11-09)

# Item Action
1 Un-replayed 12/12 firmware-oracle claim Removed from CHANGELOG.md, automation.py (module and class docstrings) and tests/test_who2_audit_fixes.py; the docstring now says explicitly that neither frame has been replayed in own-firmware-oracle.
2 require_non_empty_where -> require_address_where All seven builders switched; tests cover True, False, None, 12.5, b"31", ["31"], "31##", "31 1", "31\t1", "31\n1" (rejected) and " 0 ", "31\n" (stripped and accepted, as require_address_where does for WHO 1).
3 Dead AUX_WHAT_NAMES / AUX_WHAT_NAMES_UNVERIFIED Deleted; caveat kept in the OWNAuxEvent / channel docstrings.
4 Docstring vs committed traces Rewritten around issue_445, issue_466 and f461_live_2026-11-04 as described above; 13 / 14 noted as ACKed and translated by the F461 2.0.11.
5 Non-ASCII digits report_shutter_level priority string requires .isascii(); OWNAutomationEvent left untouched (zero decode diff).
6 Merge state, title, body, scratch tags Brought up to master 32cca9e (merge 4cdf1a8, tree = rebased commit 9dc1e45); title and body rewritten to the actual diff; test_a4_a5_* / test_a9_* / rows A4, A5, A9 / D56 labels removed; sync_documentation.py --check, verify_library_standards.py, validate_corpus.py (166) re-run.

On the follow-up question: agreed, no separate PR re-proposing OWNAutomationEvent advanced-WHAT decoding. The MyHOME f461_live_2026-11-04/README.md wording that conflates the F461's 1000# translations with F411/2 actuator support will be corrected in MyHOME, not here.

Testing

  • ruff check OWNd tests setup.py scripts, mypy OWNd tests/typing, python scripts/verify_library_standards.py, python scripts/sync_documentation.py --check, python tools/golden/validate_corpus.py (166 frames, PASS).
  • python -m pytest -q: 3194 passed; tests/test_who2_audit_fixes.py: 111 cases.
  • MyHOME v2-phase1-architecture (6c50e358) test suite with this branch on PYTHONPATH, the way myhome-conformance runs it: see the follow-up comment for the result line.

🤖 Generated with Claude Code

gdluck and others added 6 commits November 3, 2026 09:45
…ty and info

Review-session audit automation_load_aux.md rows A1, A2, A3, A4, A5, A6, A7,
A8, A9 (ENCY_CONFIRM AU-A1..AU-A9: A4, A5, A6, A7, A9 CONFIRMED, A1, A2, A3,
A8 FUNCTIONAL-ONLY with the on-wire LN4660M2 frames as MyHOME traces).

Encyclopedia who-2-automation/what.md#advanced-commands, #parameter-format
and #observed-physical-transmitter-frames: *2*10#PRIORITY[#SELECTOR]*WHERE##
is Advanced Stop, *2*11#STEP#PRIORITY[#SELECTOR]*WHERE## / *2*12#... advanced
Up / Down with STEP 1..99 or 100 (end point) and a 3-bit priority; captured as
*2*11#100#001#1*0##, *2*12#100#001#1*0##, *2*10#001#1*0##. They are now
decoded into is_advanced_stop / is_advanced_up / is_advanced_down, step,
moves_to_end_point, priority, priority_bitfield, selector and no longer copied
into the Dimension 10 state (no position, no is_closed claim, no "None" in
the log). what.md#state-values-are-not-commands and
dimensions.md#shutter-status: *2*13*W## / *2*14*W## are not commands and
fail closed (unknown_command_code). dimensions.md#shutter-priority and
#shutter-information: Dimension 10 replies expose priority (int and 3-digit
bit field) and info (0 / 12 / 13 / 14 / 15 with names).
dimensions.md#resulting-state-traffic: the Dimension 11 operation and its
F461 2.0.11 echo *michnovka#2*25*OpenWebNet-HA#11#001#1*50## (golden
cover.evt.level.50.virtual.25.f461) are event traffic, routed by
OWNMessage.parse to OWNAutomationEvent with target_level.
report_shutter_level accepts states 10..14 only, emits the priority as the
p1p2p3 bit field and logs 255 as an unknown position. addressing.md#where-forms:
every WHO 2 builder refuses an empty or '*' WHERE.

Golden corpus: dropped the legrand-spec fixture cover.dim.write.pos.75
(*michnovka#2*93*OpenWebNet-HA#10*75##): dimensions.md lists Dimension 10 as request / response /
event only and the position write as *michnovka#2*WHERE*OpenWebNet-HA#11#PRIORITY*LEVEL##
(ENCY_CONFIRM AU-A4, protocol/dimensions.md#writes). Relabelled
cover.dim.write.tilt.30 as cover.dim.write.level.30.no_priority (Dimension 11
is absolute position; no slat-angle dimension exists, ENCY_CONFIRM AU-A5) and
cover.evt.level.50.virtual.25.f461 as direction event; corpus.json
regenerated with tools/golden/validate_corpus.py. README, the conformance
matrix and the golden README no longer say "tilt angles" / "Slat Tilt".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ichnovka#3##

Review-session audit automation_load_aux.md row L1 (ENCY_CONFIRM AU-L1:
OWNd-SIDE, internal docstring / parser contradiction; ORACLE_CONFIRM row L1:
CONFIRMED).

load.py documented that bt_energia only accepts *michnovka#3## and *michnovka#3*## for general
status, yet OWNMessage.parse('*michnovka#3*##') returned None and no builder existed.
The 2026-11-03 oracle run (ORACLE_CONFIRM.md row L1) shows the documented
*michnovka#3*0## (Encyclopedia who-3-load-management/README.md#status-requests)
refused on 12/12 emulated gateways, *michnovka#3## out as $03AA001614 on 8 of them and
*michnovka#3*## out on F454 / F459 only. base.py adds a WHO 3-only _LOAD_GENERAL_STATUS
regex (*michnovka#3*## -> STATUS_REQUEST with where "") and OWNCommand.parse's WHO
pre-screen accepts an empty last field; OWNLoadCommand logs "Requesting
general load status." for both spellings and general_status() builds *michnovka#3##.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review-session audit automation_load_aux.md row X1 (ENCY_CONFIRM AU-X1:
CONFIRMED).

Encyclopedia who-9-auxiliaries/README.md#corpus-status establishes only
*OpenWebNet-HA#9*WHERE##, *9*1*WHERE## (ON) and *9*0*WHERE## (OFF); "Unknown values
remain unspecified rather than inferred" and the TS10 decoder "rejects other
WHAT cases"; it "does not establish an auxiliary-address range". OWNAuxEvent
keeps the openwebnet4j names for WHAT 2..10 (AUX_WHAT_NAMES_UNVERIFIED) but
flags them as unverified in the log and in the new is_state_name_verified
property; state_name exposes the name; channel is documented as the WHERE as
sent. The README WHO 9 row (scripts/sync_documentation.py catalog) no longer
says "aux 1-9".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re (decision D56, D21)

Precedent gate for report 21 rows A6 and A8.

A6: `OWNMessage.parse` no longer routes `*michnovka#2*WHERE*OpenWebNet-HA#11#PRIORITY[#SELECTOR]*LEVEL##`
to `OWNAutomationEvent`. DIMENSION 11 is write-only for WHO 2 (MyHOME #797
review, D56) and MyHOME's pinned suite asserts `OWNAutomationCommand` for the
scope echoes `*michnovka#2*michnovka#5*OpenWebNet-HA#11#001#1*50##` / `*michnovka#2*0*OpenWebNet-HA#11#001#1*50##`
(tests/test_gateway.py test_scope_events_fire_only_for_state_frames), so a
general / area / group go-to-level echo can never reach the WHO 2 motion
event branch of gateway_events.py. The command object now decodes
`target_level`, `priority`, `priority_bitfield`, `selector` and
`is_go_to_level` (who-2-automation/dimensions.md#dimension-11---go-to-level);
`OWNEvent.parse` called directly on the frame still exposes the same fields.
Golden `cover.evt.level.50.virtual.25.f461` keeps `direction: dimension_write`
(corpus.json regenerated with tools/golden/validate_corpus.py).

A8: `report_shutter_level` emits an int priority as its decimal text, as
before (`*michnovka#2*21*10*12*40*0*0##`, pinned by MyHOME tests/test_expose.py); the
3-digit `p1p2p3` bit field is the string form, which is still validated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ision D21)

Precedent gate for report 21 row A8: MyHOME tests/test_expose.py
(test_expose_infeasible_inputs_hardening) pins
`report_shutter_level("31", 40, state=10, priority="00001")` ->
`*michnovka#2*31*10*10*40*1*0##`, so the string form keeps its pre-existing
handling (up to three digits sent as given, otherwise the decimal int 0..255)
instead of requiring a 3-digit bit field. The state 10..14 / info / 255 log
parts of the row stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…iority bitfield rule (review follow-up)

Review follow-up on report 21 rows L1, A2, A7/A8.

- `OWNCommand.parse` pre-screen is back to `(?:\*.+)?##$`; the empty last
  field is admitted only through `_LOAD_GENERAL_STATUS` (`*michnovka#3*##`), so
  `*michnovka#2*##`, `*OpenWebNet-HA#13*##`, `*michnovka#4*##` stay `None` as on master.
- `_decode_advanced`: the WHAT grammar fixes the first field as STEP
  (who-2-automation/what.md#parameter-format), so a single parameter is the
  STEP with no priority: `*2*11#100*31##` -> step 100, end point;
  `*2*11#50*31##` -> step 50.
- `_priority_bitfield`: exactly three `0`/`1` characters are the bit field
  (what.md#parameter-format, dimensions.md#shutter-priority); the page records
  no shortened form, so `1`, `7`, `10` are decimal priorities with
  `priority_bitfield None`. `report_shutter_level(priority=10)` -> `*10*`
  parses back as priority 10, not bit field 010.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@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!

… not the published event grammar

The WHO 2 PDF §3.0.7.1 publishes the event-session frame as
*michnovka#2*WHERE*OpenWebNet-HA#11#PRIORITY#LEVEL*SELECTOR## (LEVEL as the second # parameter); the
F461 2.0.11 echoes the write form *michnovka#2*WHERE*OpenWebNet-HA#11#PRIORITY[#SELECTOR]*LEVEL##,
which is what OWNd decodes. CHANGELOG and two docstrings say so. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck marked this pull request as ready for review November 4, 2026 13:45

@GreenGrassBlueOcean GreenGrassBlueOcean 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 for auditing WHO 2, WHO 3, and WHO 9 and explicitly noting in the PR description that the Encyclopedia's WHO 2 captures are MyHOME's own captures rather than independent traces of the advanced commands.

Under 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 to the non-empty-WHERE builder guards and pure documentation fixes, and revert the doc-derived runtime changes.

What can stay

  1. Non-empty WHERE guards on OWNAutomationCommand builders (A5): Rejecting empty where in up, down, stop, set_position, status, and advanced_status (*2*1*##, *#2**10## are NACKed 12/12 in the firmware oracle), while keeping OWNCommand.parse permissive.
  2. Documentation & "Slat Tilt" cleanup: Removing the unverified "Slat Tilt" wording from README.md, docs/protocol_conformance_matrix.md, scripts/sync_documentation.py, and tests/golden/README.md, plus docstring notes on WHO 2 / WHO 9 specification caveats.

Blockers — Behaviour changes without committed plant traces (please revert)

# Symbol / File Behaviour change without real plant trace Required action
1 OWNAutomationEvent (WHAT 10, 11, 12 & WHAT 13, 14) in OWNd/message/automation.py Changes state (10/11/12 -> None) and is_closed (False -> None) on WHAT 10 / 11 / 12, changes WHAT 13 / 14 from state = 13/14 (is_opening / is_closing) to unknown_command_code (None), and adds speculative step, moves_to_end_point, priority_bitfield, priority_safety/high/medium, and selector decoders from WHO_2.pdf (ownkb:claim:c007501, c007502) with no independent physical advanced-shutter plant traces. Revert OWNAutomationEvent runtime decoding and human_readable_log changes to master until independent on-wire captures of advanced WHO 2 shutter flows are committed.
2 OWNAutomationCommand.report_shutter_level & DIMENSION 10 decoding in OWNd/message/automation.py Rejects state=15 and info outside {0, 12, 13, 14, 15} in report_shutter_level (forcing edits to test_expose_builders_infeasible_inputs in tests/test_message_protocol.py), drops DIMENSION 10 state outside 10..14, and appends info_name / alters 255 human_readable_log strings without trace evidence. Revert report_shutter_level validation/log changes and DIMENSION 10 decoding changes to master.
3 cover.dim.write.pos.75 (*#2*93*#10*75##) in tests/golden/frames/who02_automation.yaml & tests/golden/corpus.json Deletes cover.dim.write.pos.75 from the golden corpus. Do not delete golden parser fixtures: keep the record in who02_automation.yaml / corpus.json and relabel it source: conflict with updated mcp_notes (the same pattern used in #157 for who18_energy.yaml).
4 _LOAD_GENERAL_STATUS in OWNd/message/base.py & OWNLoadCommand.general_status in OWNd/message/load.py (WHO 3) Adds *#3*## special-casing in OWNMessage / OWNCommand and adds OWNLoadCommand.general_status() (*#3##) with zero physical F520 / F521 / F522 / F523 load-management plant captures (empty-bus gateway forwarding does not establish device replies). Revert base.py and load.py runtime additions until we have a real WHO 3 load-management capture.
5 OWNAuxEvent.human_readable_log (WHO 9) in OWNd/message/auxiliary.py Appends (WHAT N, unverified name) to human_readable_log for WHAT 2..10. Revert the runtime human_readable_log change; keep the provenance caveat in AUX_WHAT_NAMES comments / OWNAuxEvent docstrings.

gdluck and others added 4 commits November 4, 2026 16:49
…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>
@gdluck

gdluck commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Round 2 (review of 2026-11-09) applied in 4cdf1a8: a merge of master 32cca9e into the branch whose tree is byte-identical to the single rebased commit 9dc1e45 (git diff master is the same; a force-push was not possible from the session that pushed, so the history is not rewritten; a squash merge gives the same result, and I can force-push the rebased commit instead if you prefer). The description is rewritten to the actual diff.

# Item Action taken
1 Un-replayed 12/12 firmware-oracle claim Removed from CHANGELOG.md, automation.py and the tests. Checked own-firmware-oracle main (*.cases, results) and tests/golden/firmware_oracle.json on master: no WHO 2 empty-WHERE frame anywhere; the docstring now says so and presents the guards as argument validation.
2 require_address_where on all seven builders Done, including set_shutter_level and report_shutter_level. Rejected: True, False, None, 12.5, b"31", ["31"], "31##", "31 1", "31\t1", "31\n1". "31\n" strips to 31 and is accepted, exactly as require_address_where behaves for WHO 1 (" 12 "), so it is pinned as accepted rather than rejected.
3 Dead WHO 9 globals AUX_WHAT_NAMES / AUX_WHAT_NAMES_UNVERIFIED deleted; provenance caveat kept in the OWNAuxEvent / channel docstrings.
4 Docstring vs committed traces Rewritten around issue_445 (LN4660M2 centralised *2*11#100#001#1*0## / *2*12#100#001#1*0## / *2*10#001#1*0##), issue_466 (F454 relative steps *2*11#STEP#001*31## / *2*12#STEP#001*31##, STEP 1 / 2 / 10, 1000# echoes, actuator *2*1*31## / *2*2*31## / DIMENSION 10 / *2*0*31##) and f461_live_2026-11-04 (F461 2.0.11 ACKs *2*13*05## / *2*14*05##, translates *2*1000#13#001#1*05## / *2*1000#14#001#1*05##; the F411/2 at 05 emits nothing for the five advanced commands). One detail I could not confirm in the committed log: it holds no *2*15*05## frame, so the NACK on WHAT 15 is not cited.
5 Non-ASCII digits report_shutter_level keeps a string priority only when .isascii() and .isdigit() ("001" -> *1*, pinned). OWNAutomationEvent untouched: zero decode diff.
6 Merge state / title / body / scratch tags Up to date with master (merge 4cdf1a8, tree = rebased 9dc1e45), mergeable; title and body rewritten; test_a4_a5_*, test_a9_*, rows A4, A5, A9, D56 removed; sync_documentation.py --check, verify_library_standards.py, validate_corpus.py (11 files, 166 frames) green; golden README count 166 is now asserted by a test.

Checks: ruff, mypy OWNd tests/typing, pytest 3194 passed (111 in tests/test_who2_audit_fixes.py); MyHOME v2-phase1-architecture 6c50e358 suite with this branch on PYTHONPATH: 5565 passed, 1 deselected (the known test_check_all_documentation_real_tree pin signal).

Agreed on the follow-up: no separate PR for OWNAutomationEvent advanced-WHAT decoding. The f461_live_2026-11-04/README.md wording in MyHOME (gateway 1000# translation vs F411/2 support) will be fixed in MyHOME.


Round 1 (2026-11-04), applied in `fe3c463`
Blocker Action taken
1. OWNAutomationEvent WHAT 10 / 11 / 12, 13 / 14 and DIMENSION 11 automation.py rebuilt from master: state, is_closed, is_opening / is_closing as before, no unknown_command_code; step, moves_to_end_point, priority_bitfield, priority_safety / high / medium, selector, info_name, is_command_not_executed, target_level, is_go_to_level and their logs removed (the same DIMENSION 11 decoding / log change on OWNAutomationCommand.__init__ reverted too, since a log string on a valid frame is runtime behaviour).
2. report_shutter_level / DIMENSION 10 master restored (state 10..15, priority / info 0..255, master's 255 log, state outside 10..14 decoded as before); test_message_protocol.py restored verbatim.
3. cover.dim.write.pos.75 Restored at its original position, relabelled source: conflict with mcp_notes explaining that WHO_2.pdf / the Encyclopedia define DIMENSION 10 as request / response / event only and that no committed trace carries the frame.
4. WHO 3 base.py and load.py restored from master; tests pruned.
5. WHO 9 master's human_readable_log chain restored verbatim; the (WHAT N, unverified name) suffix and the state_name / is_state_name_verified properties removed.

@gdluck
gdluck marked this pull request as draft November 4, 2026 15:14
@gdluck
gdluck marked this pull request as ready for review November 4, 2026 16:45

@GreenGrassBlueOcean GreenGrassBlueOcean 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.

Checked fc34e2c against current master (32cca9e6), own-firmware-oracle, the committed MyHOME traces (issue_445, issue_466, and MyHOME#865), and downstream MyHOME consumers (cover.py, expose.py, gateway_events.py).

Does this PR solve a real problem?

  • Runtime (OWNAutomationCommand WHERE guards): Marginal API hygiene only — fixes no bug or crash. On master, status("") (*#2*##), raise_shutter("") (*2*1*##), lower_shutter("") (*2*2*##), and stop_shutter("") (*2*0*##) already parse with is_valid is False and are blocked before touching the socket or UART by OWNCommandSession.send (OWNd/connection/command_session.py:283) and AsyncSerialTransport.send (OWNd/transport/serial.py:173) with OWNProtocolError(..., reason="invalid_frame"). The only is_valid is True frames previously reachable from those builders were get_shutter_status("") (*#2**10##, accepted by base.py only because WHO 13 uses *#13**0##), where="*" (*2*1**##, accepted only because WHO 5 uses *), and delimiter injection (status("31*10") -> *#2*31*10##). Every MyHOME caller (cover.py, expose.py) passes a pre-validated WHERE, and unlike WHO 1 *#1*## (MH202 crash, #155) or WHO 4 Dimension 22 (bt_termo crash, #156), there is no firmware crash vector here. Fail-early builder validation is fine for parity with #155, provided it uses master's require_address_where rather than the superseded require_non_empty_where.
  • Documentation & golden metadata (WHO 2): Yes (errata cleanup). OpenWebNet WHO 2 has no wire dimension for slat tilt (MyHOME's Venetian blind tilt in cover.py is purely client-side timed motor-pulse estimation over standard UP / DOWN / STOP), and WHO_2.pdf defines no Dimension 10 write (*#2*93*#10*75##). Relabelling cover.dim.write.pos.75 as source: conflict, renaming/clarifying cover.dim.write.tilt.30, and removing "Slat Tilt" / "tilt angles" from WHO 2 wire-protocol docs fixes real documentation drift.
  • WHO 9 module globals (AUX_WHAT_NAMES, AUX_WHAT_NAMES_UNVERIFIED): No. Dead unexported globals referenced nowhere in OWNd or MyHOME.

Summary of Required Changes

# Severity Location Issue Required Action
1 Blocker CHANGELOG.md:8, OWNd/message/automation.py:24-25, 177-178, tests/test_who2_audit_fixes.py:4-5, 76 States 6× as empirical fact that *2*1*## and *#2**10## "are NACKed by all twelve emulated gateways in the firmware oracle" — neither frame exists in own-firmware-oracle or tests/golden/firmware_oracle.json. Remove the unverified 12/12 firmware-oracle claim everywhere (cite WHO_2.pdf / Encyclopedia who-2-automation/addressing.md#where-forms), or actually replay *2*1*##, *#2*##, and *#2**10## in own-firmware-oracle first and cite the real suite.
2 High OWNd/message/automation.py:38, 185-228, 264 Uses require_non_empty_where instead of master's require_address_where (#155): raise_shutter(True) passes mypy --strict (bool <: int) and builds invalid *2*1*True## (where=None, log "Raising shutter None."); status(None) -> *#2*None##; raise_shutter("31##") -> *2*1*31####; raise_shutter("31 1") -> *2*1*31 1##. Switch all seven OWNAutomationCommand builders (set_shutter_level and report_shutter_level included) to require_address_where, and test bool, None, "31##", "31 1", and "31\n".
3 Medium OWNd/message/scenario.py:75-94 AUX_WHAT_NAMES and AUX_WHAT_NAMES_UNVERIFIED are dead globals (not in OWNd/message/__init__.py __all__, not used by OWNAuxEvent.__init__, not imported anywhere). Delete both dicts; keep the provenance caveat in the OWNAuxEvent / channel docstrings only.
4 Medium OWNd/message/automation.py:4-5, 11-12, tests/test_who2_audit_fixes.py:8 Claims "no committed on-wire plant trace exercises the advanced forms" (contradicted by MyHOME#445 and MyHOME#466) and states unqualifiedly that 13 / 14 are "only DIMENSION 10 state values" (contradicted by F461 2.0.11 translating *2*13*05## / *2*14*05## in MyHOME#865). Reword both docstring notes to match the actual committed plant captures.
5 Medium OWNd/message/automation.py:67-95, 280-286 Non-ASCII digit leak vs #155 / #156: OWNAutomationEvent (DIMENSION 10) decodes fullwidth Unicode digits (*#2*21*10*12*40*001*14## -> state=12, current_position=40), and report_shutter_level("31", 40, 10, "001", 0) emits non-ASCII wire bytes because .isdigit() lacks .isascii(). At minimum harden report_shutter_level's string priority check with .isascii() (and either guard DIMENSION 10 with .isascii() and .isdigit() for #155/#156 parity or leave OWNAutomationEvent untouched if keeping zero decode diff here).
6 Blocker Branch merge state + PR Title / Body + tests/test_who2_audit_fixes.py Branch is 30 commits behind master (mergeable_state: "dirty", conflicts in 6 files); PR title, summary, changes list, state diagram, and test count ("79 cases") still describe the reverted b931362 design; test file retains private scratch tags (rows A4, A5, A9, D56, test_a4_a5_*). Rebase onto master, rewrite PR title and body to match the actual diff, re-run sync_documentation.py / corpus validation (verify fixture count in tests/golden/README.md), and strip scratch audit labels from tests/test_who2_audit_fixes.py.

Detailed Notes & Answer on the Follow-Up Proposal (#6633949587 / MyHOME#865)

  1. Unverified firmware-oracle citation (*2*1*##, *#2**10##):

    • Checked own-firmware-oracle/suites/*.cases (ownd-audit-frames.cases only tested WHO 1 *#1*##, WHO 3, WHO 17, WHO 18, WHO 22; trace-corpus.cases only tested populated WHO 2 WHERE values), own-firmware-oracle/results/**/*.tsv, and tests/golden/firmware_oracle.json: zero WHO 2 empty-WHERE frames have been replayed. As your original PR body noted ("The WHO 2 rows are decode-side"), the 12/12 parenthetical from Round 1 review text was never replayed in the oracle and must not be recorded as an oracle verdict in docstrings, tests, or CHANGELOG.md.
  2. Corrections on the cited MyHOME traces (issue_445, issue_466, and MyHOME#865):

    • issue_445 vs issue_466: issue_445 (MyHomeServer1 + LN-4660M2) carries centralized General WHERE=0 frames (*2*11#100#001#1*0##, *2*12#100#001#1*0##, *2*10#001#1*0##), not *2*11#1#001*31## / *2*12#1#001*31##. The WHERE=31 step frames (10%, 2%, 1%) are HA-originated commands in issue_466/config_entry-myhome_F454_step_commands.json (which F454 echoed as *2*1000#12#10#001#1*31##, while the actuator at 31 reported standard *2*2*31##, *2*0*31##, and Dimension 10 *#2*31*10*12*40*001*0## -> *#2*31*10*10*30*001*0##).
    • MyHOME#865 (f461_live_2026-11-04/F461_full_debug_log_2026-11-04T15-45-56.txt): Address 05 is a basic timed BT-F411/2 relay shutter, not an advanced actuator. Compare lines 583–596 (*2*2*05## -> gateway *2*1000#0*05##, actuator *2*0*05##, gateway *2*1000#2*05##, actuator *2*2*05##) against lines 607–650 (*2*11#1#001*05##, *2*12#1#001*05##, *2*10#001#1*05##, *2*13*05##, *2*14*05##): on all five advanced commands, only the F461 gateway's bt_autom daemon emitted *2*1000#...*05## command translations — the F411/2 at 05 emitted zero status frames and ignored every one of them. (Please also correct f461_live_2026-11-04/README.md:56 in MyHOME so gateway command translation is not conflated with F411/2 actuator support.)
    • WHAT 13 and WHAT 14 on F461 2.0.11: Lines 637–650 of that same log show F461 2.0.11 ACKing *2*13*05## and *2*14*05## and translating them onto the monitor session as *2*1000#13#001#1*05## and *2*1000#14#001#1*05## (while NACKing *2*15*05##). That confirms bt_autom treats WHAT 13 and WHAT 14 as valid command WHATs despite WHO_2.pdf listing 13 / 14 only under Dimension 10 states — directly illustrating why failing closed on 13 / 14 from the PDF alone would have broken real gateway traffic.
  3. Should a separate PR re-propose OWNAutomationEvent advanced-WHAT decoding?

    • No. Downstream MyHOME (cover.py and gateway_events.py) already handles issue_445's *2*10/11/12#...*0## frames through is_opening / is_closing, already accounts for effective_what in (1, 2, 11, 12, 13, 14) on 1000# translations (cover.py:2576), and deliberately ignores Dimension 11 frames (cover.py:2582-2590) because shutter position comes from the actuator's Dimension 10 reports rather than command echoes. Pinning the existing issue_445 / issue_466 frames as community-plant-capture golden records against master's current decoding is fine if desired, whereas adding unused properties (step, moves_to_end_point, priority_bitfield, selector) or changing state to None solves no real problem.

Comment thread CHANGELOG.md Outdated
## [Unreleased]

### Changed
- **WHO 2 (`OWNAutomationCommand.status`, `get_shutter_status`, `raise_shutter`, `lower_shutter`, `stop_shutter`)**: an empty, blank or `*` WHERE raises `ValueError` as `set_shutter_level` and `report_shutter_level` already did; `*2*1*##` and `*#2**10##` are NACKed by all twelve emulated gateways in the firmware oracle, so the builders no longer emit them. `OWNCommand.parse` stays permissive and no decoded value changes.

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.

Neither *2*1*## nor *#2**10## has been replayed in own-firmware-oracle or tests/golden/firmware_oracle.json (ownd-audit-frames.cases only tested WHO 1 *#1*##). Drop the all twelve emulated gateways in the firmware oracle claim here and in automation.py / test_who2_audit_fixes.py unless those frames are actually added to own-firmware-oracle and replayed, and update this bullet to reflect require_address_where across all seven OWNAutomationCommand builders.

Comment thread OWNd/message/automation.py Outdated

Specification caveats (documentation context only; the decoding below is the
long-standing behaviour and is not changed on the strength of these notes, as
no committed on-wire plant trace exercises the advanced forms):

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.

Two factual fixes needed in this docstring block:

  1. Line 5 ("no committed on-wire plant trace exercises the advanced forms"): MyHOME has committed traces exercising *2*11#100#001#1*0##, *2*12#100#001#1*0##, *2*10#001#1*0## (issue_445, MyHomeServer1 + LN-4660M2) and *2*11#10#001*31## / *2*12#10#001*31## / *2*12#2#001*31## / *2*12#1#001*31## (issue_466, F454 + 1000# echoes + Dimension 10 replies). Reword to reflect what is vs isn't captured on-wire.
  2. Lines 11–12 (13 / 14 "listed only as DIMENSION 10 state values") & lines 24–25: note that while WHO_2.pdf lists 13 / 14 only under Dimension 10, physical F461 2.0.11 (bt_autom) ACKs *2*13*W## / *2*14*W## and translates them as *2*1000#13#001#1*W## / *2*1000#14#001#1*W## (MyHOME#865), and remove the un-replayed firmware oracle NACKs *2*1*## / *#2**10## claim on lines 24–25.

Comment thread OWNd/message/automation.py Outdated
@classmethod
def status(cls, where: str | int) -> OWNAutomationCommand:
message = cls(f"*#2*{where}##")
target = require_non_empty_where(where)

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.

After rebasing onto master, use require_address_where(where) (added in #155) instead of require_non_empty_where(where) across all seven OWNAutomationCommand builders (status, get_shutter_status, raise_shutter, lower_shutter, stop_shutter, set_shutter_level, and report_shutter_level). require_non_empty_where still lets bool (raise_shutter(True) passes mypy --strict and builds invalid *2*1*True##), None, "31##", and "31 1" slip through. Also drop the un-replayed firmware-oracle claim in the class docstring above (lines 177–178).

Comment thread OWNd/message/scenario.py Outdated

#: WHAT names the Encyclopedia establishes for WHO 9 (``who-9-auxiliaries/README.md#corpus-status``):
#: ``*9*1*WHERE##`` ON and ``*9*0*WHERE##`` OFF only.
AUX_WHAT_NAMES: dict[int, str] = {0: "OFF", 1: "ON"}

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.

Neither AUX_WHAT_NAMES nor AUX_WHAT_NAMES_UNVERIFIED is exported in OWNd/message/__init__.py (__all__), referenced by OWNAuxEvent.__init__ below, or used anywhere else. Please remove both unused module globals and keep the provenance caveat in the OWNAuxEvent / channel docstrings only.

Comment thread tests/test_who2_audit_fixes.py Outdated
@@ -0,0 +1,98 @@
"""WHO 2 audit follow-ups that change no decoded behaviour (review-session rows A4, A5, A9).

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.

Strip the private review-session scratch tags (review-session rows A4, A5, A9, # ── A4 / A5:, (gate, D56), test_a4_a5_*, test_a9_*) and the un-replayed NACKed 12/12 in the firmware oracle comment here and on line 76, and expand the rejected-WHERE parametrization to cover True, False, None, "31##", "31 1", and "31\n" once switched to require_address_where.

@gdluck gdluck changed the title fix(automation): WHO 2 advanced WHATs 10/11/12 decoded, 13/14 fail closed, priority and info exposed, builders require a WHERE; WHO 3 general request; WHO 9 names marked unverified (Encyclopedia audit, group 2) fix(automation): WHO 2 builders validate WHERE with require_address_where, ASCII-only priority string; WHO 2 / WHO 9 documentation and golden-corpus errata (Encyclopedia audit, group 2) Nov 9, 2026
… tree as 9dc1e45)

The result is byte-identical to the single rebased commit 9dc1e45
(kept locally as backup/who2-rebased-9dc1e45): WHO 2 builders use
require_address_where, ASCII-only priority string, un-replayed oracle
claim and dead WHO 9 globals removed, docstrings on the committed
captures, scratch tags stripped, corpus regenerated (166), golden README
count 166. A history rewrite was not possible from this session, so the
branch is brought up to master by merge; a squash merge gives the same
tree.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants