Skip to content

fix(sound): refuse the unpublished bare WHO 22 power frame in the send layer; deprecate turn_on / turn_off - #163

Open
gdluck wants to merge 1 commit into
OpenWebNet-HA:masterfrom
gdluck:fix/who22-bare-power-guard-on-158
Open

gdluck wants to merge 1 commit into
OpenWebNet-HA:masterfrom
gdluck:fix/who22-bare-power-guard-on-158

Conversation

@gdluck

@gdluck gdluck commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Requested by A

Review follow-up (GreenGrassBlueOcean, 2026-11-04) — rebuilt as one commit on master

# Required action Done
1 Rebase onto master; reference master builders The branch is now a single commit on master (no #158 history). DESTRUCTIVE_WHO22_REASON, the turn_on / turn_off docstrings and the tests name turn_on_with_source / route_source / turn_off_atomic; turn_on_atomic is gone.
2 Committed oracle crash provenance own-firmware-oracle#78 adds oracle/cases/sound-who22-bare-power.cases with result tables for all twelve emulated gateways (two byte-identical runs) and the regenerated mcp_index.json; tests/golden/firmware_oracle.json here pins the same 144 rows (520 inputs, 6,644 rows, 23 suites, digest recomputed) and the conformance test pins the 64 crash rows to exactly that suite, frame shape and gateway set.
3 No runtime DeprecationWarning Removed; turn_on / turn_off build the same frames as on master and document the crash and the send-layer refusal in their docstrings.

What the suite showed, wider than the first run: the bare form crashes the sound daemon on eight of the twelve emulated gateways (F453, F453AV, F454, F459, F460, F461, MH200N, MyHomeServer1), whatever the WHERE class; the published *22*1#4#1*WHERE## goes out on the bus on the same eight and on MH202; F450, H4684 and MH200 drop both. The bare form was never sent to physical hardware; the published form was ACKed by the live F461 2.0.11 (comment above). The guard also covers the serial transport, as the WHO 4 dimension 22 guard does.

Stacked on #158. Please merge #158 first; only the commits beyond its head belong to this PR, because the safe replacements (turn_on_atomic / turn_off_atomic) live there. Until #158 merges the diff tab shows both.

Before: OWNSoundDiffusionCommand.turn_on / turn_off built the bare WHO 22 power frame, WHAT 0 / 1 with no #mmtype#area parameters (*22*1*WHERE##, *22*0*WHERE##), and every send path let it through.

After: every send path refuses the bare frame with reason="destructive_frame", exactly as the WHO 4 dimension 22 and WHO 18 dimension 52 crash frames are refused; turn_on / turn_off still build it for callers that pin the frame but issue a DeprecationWarning pointing to turn_on_atomic / turn_off_atomic. No new builder is added. OWNCommand.parse stays permissive (decision D1).

Why: the bare form is not published. WHO 22 PDF 3.1.1 / 3.1.2 give *22*0#<mmtype>#<area>*2#<sourceID>## / *22*1#<mmtype>#<area>*2#<sourceID>## and 3.2.1 gives *22*0#<mmtype>#<area>*3#<area><point>## (printed in the PDF without the # between the parameters; the tested form has it), with <area> the output area 1..9. In the firmware oracle (2026-11-04, two byte-identical runs each) the bare form crashes the sound daemon on F461 020011 and F454 020051 on every tested target type — source 2#S, amplifier 3#A#P, area 4#A, all sources 6 (6 is "All sources" in the WHO 22 WHERE table; General is 5#SENDER) — while MH202 refuses it without crashing, and the published forms go out (ack/out) on every target. The PDF also confirms the reading #158 already takes: the second parameter is the area, not the source, which is why no *_with_source OFF twin is added.

MyHOME is unaffected: it powers audio zones through WHO 16 (OWNSoundCommand.turn_on → *16*3*WHERE##, turn_off → *16*13*WHERE##) and never builds the WHO 22 form; its conformance suite passes in full against this branch.

How: is_who22_bare_power_frame() detector in sound.py (exported), the refusal helper and error take a reason string, both send entry points check it after the dimension 22 detector. Tests: detector on every target, published forms not detected and reaching the write through both send paths (connection mocked), deprecated builders warn and keep their frame, both session entry points refusing before any connect. Full suite 2878 passed, mypy clean. Second opinion (DeepSeek) taken twice; its area-vs-source catch is what removed the OFF builder.

🤖 Generated with Claude Code

@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!

@gdluck

gdluck commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Live on the F461 2.0.11 test plant (2026-11-04):

  • The published with-source power frames are accepted: *22*1#4#1*2#1## -> ACK, *22*0#4#1*2#1## -> ACK (no audio device attached, so no state change; the gateway stayed responsive).
  • The bare form this PR guards is never put on the wire: with the guard in place, OWNCommandSession.send("*22*1*2#1##") and send("*22*0*6##") return None with reason="destructive_frame" before any socket is opened, verified against a real session object. The bare form's crash was characterised on the emulated fleet earlier and deliberately not fired at the plant.

@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 investigating the F461 020011 / F454 020051 sound-daemon crash on bare *22*0*WHERE## / *22*1*WHERE## frames.

Preventing proven gateway-daemon crashes in the send layer (reason="destructive_frame", while keeping OWNCommand.parse permissive) fits the same safety exception as is_who4_dimension_22_crash_form and #157's 52#YYYY#M guard. However, under our standing rule — if we don't have real traces (or committed, reproducible oracle crash records for crash guards), we don't change behaviour — three changes are needed before this guard can land:

# Area / Symbol Issue Required action
1 Branch base & replacement references (sound.py, command_session.py, test_who22_bare_power_guard.py) Stacked on #158 and references turn_on_atomic in DESTRUCTIVE_WHO22_REASON, docstrings, and tests. #158's runtime WHO 22 / WHO 16 changes (including turn_on_atomic and area-derived turn_off_atomic) lack physical sound-plant traces and have changes requested. Rebase directly onto master so the crash guard stands on its own, and reference existing master builders (turn_on_with_source / route_source / turn_off_atomic) in DESTRUCTIVE_WHO22_REASON and docstrings rather than turn_on_atomic.
2 Missing committed oracle crash provenance (firmware_oracle.json / own-firmware-oracle) lookup_firmware_verdict currently has no committed rows for *22*1*2#1##, *22*0*2#1##, *22*1*3#1#1##, *22*0*4#1##, *22*1*6##, or *22*0*6## in own-firmware-oracle (mcp_index.json) or OWNd/tests/golden/firmware_oracle.json. Commit the F461 020011 / F454 020051 crash replay rows to own-firmware-oracle and pin them in OWNd/tests/golden/firmware_oracle.json so the destructive_frame refusal is backed by reproducible, hash-verified oracle evidence in-repo.
3 Runtime DeprecationWarning in OWNSoundDiffusionCommand.turn_on / turn_off (OWNd/message/sound.py) Emits warnings.warn(..., DeprecationWarning), breaking callers under filterwarnings = ["error"] (forcing pytest.warns(DeprecationWarning) edits in tests/test_sound_diffusion.py) without physical sound-plant traces settling the replacement power flow. Remove warnings.warn(..., DeprecationWarning) from turn_on and turn_off; document the F461 / F454 daemon crash and send-layer refusal in their docstrings while is_who22_bare_power_frame protects the send path.

…und-daemon crash on 8 of 12 gateways)

The bare `*22*1*WHERE##` / `*22*0*WHERE##` (WHAT 0 / 1 without the
published `#mmtype#area` parameters) crashes the sound daemon of F453,
F453AV, F454, F459, F460, F461, MH200N and MyHomeServer1 on every target
class (2#1, 3#1#1, 4#1, 6) in the firmware oracle (suite
sound-who22-bare-power, OpenWebNet-HA/own-firmware-oracle#78, two
byte-identical runs each); the published `*22*1#4#1*WHERE##` goes out on
the bus on the same eight and on MH202; F450, H4684 and MH200 drop both.
`is_who22_bare_power_frame` detects the form and the command session,
send_to_gateway and the serial transport refuse it with
reason="destructive_frame", the pattern of the WHO 4 dimension 22 guard.
`OWNSoundDiffusionCommand.turn_on` / `turn_off` build the same frames as
before (their docstrings point to the published `turn_on_with_source` /
`route_source` / `turn_off_atomic`), `OWNCommand.parse` stays
permissive, no decoded value or log line changes.

tests/golden/firmware_oracle.json pins the suite's 144 rows (12 frames x
12 emulated gateways): 520 unique inputs, 6,644 rows, 23 suites per
gateway, digest recomputed; the conformance test pins the 64 crash rows
to exactly that suite, frame shape and gateway set (no other crash, no
timeout) and the golden README numbers follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/who22-bare-power-guard-on-158 branch from 3dad2df to 28bfafc Compare November 4, 2026 15:59
@gdluck

gdluck commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Applied in 28bfafc (force-pushed as a single commit on master; the description carries the same table at the top).

# Required action Done
1 Rebase onto master; reference master builders One commit on master, no #158 history. DESTRUCTIVE_WHO22_REASON, the turn_on / turn_off docstrings and the tests name turn_on_with_source / route_source / turn_off_atomic; turn_on_atomic is gone.
2 Committed oracle crash provenance own-firmware-oracle#78 adds oracle/cases/sound-who22-bare-power.cases with result tables for all twelve emulated gateways (two byte-identical runs) and the regenerated mcp_index.json; tests/golden/firmware_oracle.json here pins the same 144 rows (520 inputs, 6,644 rows, 23 suites per gateway, digest recomputed) and test_firmware_oracle_conformance.py pins the 64 crash rows to exactly that suite, frame shape and gateway set, with no other crash and no timeout.
3 No runtime DeprecationWarning Removed; turn_on / turn_off build the same frames as on master and document the crash and the send-layer refusal in their docstrings.

The suite widened the finding: the bare form crashes the sound daemon on eight of the twelve emulated gateways (F453, F453AV, F454, F459, F460, F461, MH200N, MyHomeServer1), whatever the WHERE class; the published *22*1#4#1*WHERE## goes out on the bus on the same eight and on MH202; F450, H4684 and MH200 drop both. The guard covers the command session, send_to_gateway and the serial transport, as the WHO 4 dimension 22 guard does. The bare form was never sent to physical hardware; the published form was ACKed by the live F461 2.0.11 (comment above).

One question for you: the golden pin now carries crash rows for the first time, so the conformance test's "zero crashes" invariant became "exactly these 64 rows of this suite". If you would rather keep the golden crash-free and point the guard at the oracle repo only (as is_who4_dimension_22_crash_form does), say so and the pin comes out again.

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

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