Skip to content

fix(media_player): centralise power-state decision engine, document sound system state machine, and add intercom ducking trace replay (#682) - #683

Merged
GreenGrassBlueOcean merged 3 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
GreenGrassBlueOcean:fix/issue-682-media-player-state-complexity
Oct 10, 2026
Merged

GreenGrassBlueOcean merged 3 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
GreenGrassBlueOcean:fix/issue-682-media-player-state-complexity

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

What is the problem

The WHO 16 sound system implementation has grown into a complex state machine distributed across multiple entity layers (media_player.py, media_player_group.py, media_player_decoder.py, media_player_source.py). While modularizing the code improved readability, the interaction between amplifier power state, volume rocker commands, Dimension 1 volume status reports, video door entry (intercom ducking), multi-room grouping, and anti-hiss auto-off remained difficult to reason about and caused recurring false wake-up regressions:

  1. False wake-ups during video intercom calls (Audio amplifiers appear as "on" in Home Assistant when the video intercom stream starts or stops. #669, fix(media_player): do not wake amplifiers on Dimension 1 volume status reports (#669) #670): When a video door entry event occurs (e.g. entrance panel ring, video stream activation on Classe 100/300 internal units), the gateway or matrix broadcasts Dimension 1 volume status reports (*#16*WHERE*1*<vol>## and *#22*3#A#P*1*<vol>##) across the SCS bus to duck audio during speech and restore it afterwards. Because amplifiers retain their volume registers even while unpowered, loosely coupled status checks previously interpreted these volume reports as an indication that the amplifier was on, causing unpowered amplifiers throughout the home to falsely turn ON.
  2. Wall switch rocker interactions ([Audio]: Physical wall button activation (sound on/harder) does not update Music Assistant group membership #579): Physical volume-up rocker commands (*16*1001..1015*WHERE##) intentionally wake an amplifier, but volume-down rockers (*16*1101..1115*WHERE##) or status queries must never turn an inactive amplifier on.
  3. Scattered and fragile lifecycle logic: Power state transitions were evaluated across different if/elif branches without a centralized decision engine, creating vulnerabilities to edge cases (e.g., string vs integer dimensions from trace fixtures, spurious mock attributes, or concurrent ducking during wake sequences).
  4. Undocumented state machine: The lifecycle states of an amplifier entity (OFF, WAKE_PENDING, ON, PENDING_OFF, PARKED) and their interactions with physical hardware and bus frames were not formally documented.

How it is solved

  1. Centralised Power Decision Engine (custom_components/myhome/media_player_power.py):
    • Implemented determine_power_transition(...) as a pure, deterministic decision function with strict priority ordering:
      1. Dimension 1 Volume Reports (is_dimension_1_volume_report): Explicitly tested first. Frames matching *#16*WHERE*1*<vol>## or *#22*3#A#P*1*<vol>## return PowerTransition.NO_CHANGE. Supports both integer and string dimension representations (int(dimension) == 1).
      2. Volume-Down Rocker Guard (is_volume_rocker_down): Rocker frames *16*1101..1115*WHERE## step volume down only and return PowerTransition.NO_CHANGE.
      3. Source Events: Frames *16*3*10S## and *16*13*10S## return PowerTransition.NO_CHANGE.
      4. Explicit OFF Frames: Evaluates WAKE_ECHO_OFF (suppressed within 3.0s echo window), PARKED_CONFIRM_OFF (confirms anti-hiss off while preserving group topology), or REAL_OFF (powers off zone and coordinates group leave/handover).
      5. Explicit ON Frames: *16*3*WHERE## / *16*0*WHERE## returns PowerTransition.WAKE_ON.
      6. Volume-Up Rocker Wake: *16*1001..1015*WHERE## returns PowerTransition.WAKE_ON.
      7. Default Fallthrough: Routing frames (*16*3*1ES##), Dimension 5 queries, and unrecognised frames return PowerTransition.NO_CHANGE.
  2. Decoupled Entity Layer (custom_components/myhome/media_player.py):
    • Replaced scattered is_on / is_off / is_volume_up logic with clean dispatch to determine_power_transition(...).
    • Centralized canonical constants (WAKE_ECHO_WINDOW = 3.0s, AUTO_OFF_IDLE_DELAY = 3.0s, AUTO_OFF_PAUSED_DELAY = 60.0s, GROUP_LEAVE_GRACE = 5.0s).
  3. Comprehensive Documentation (docs/configuration/media_player.md):
    • Formally documented the 5 amplifier lifecycle states, Mermaid state transition diagram, bus frame decision table, and video door entry intercom ducking mechanics.

How it is tested

  1. Unit Tests (tests/test_media_player_power.py):
    • 100% statement coverage (79/79 statements) on media_player_power.py.
    • Parameterized tests verifying Dimension 1 reports with integer and string dimensions (dimension=1, dimension="1", _dimension=1).
    • Verification that volume status reports override spurious is_on=True.
    • Verification that volume-down rockers (1101..1115) and non-power WHAT codes (1000, 1016, 1100, 1116, 2001, 5000, etc.) return NO_CHANGE.
  2. Intercom Ducking Replay Suite (tests/test_intercom_ducking_trace_replay.py):
    • Replays the 30-frame MH202 intercom ducking capture end-to-end.
    • Verifies unpowered amplifiers stay OFF when doorbell rings, intercom call connects, volume ducks, and volume restores.
    • Verifies concurrent intercom call ducking and volume restoration during an active wake-echo window (_wake_off_sent_at).
  3. Audio Group Live Replay Suite (tests/test_audio_group_live_replay.py):
    • Multi-cycle group park and resume testing.
    • Anti-hiss auto-off verification for both owned and unowned rooms.
  4. Full Test Suite & Quality Scale:
    • 3,639 tests passed, 5 snapshots passed in 330.90s (WSL Python 3.14.4, core 2026.9.2).
    • ruff check custom_components tests scripts: 0 errors.
    • scripts/verify_ha_standards.py: Passed (Platinum quality scale satisfied).
    • scripts/sync_documentation.py --check: Passed (0 drift).

Where are the traces

  • Intercom Ducking Capture Trace:
    • File: tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_sound_intercom_ducking.json
    • Provenance & Documentation: tests/fixtures/traces/mh202_sound_intercom_ducking/README.md
    • Authentic 30-frame capture from an MH202 gateway recording a live video door entry call with hardware audio ducking and volume restoration.
  • Golden Frame Corpus & Catalog:
    • Corpus: tests/golden/corpus.json (267 calibrated frames)
    • WHO 16 frames: tests/golden/frames/who16_sound.yaml
    • WHO 22 frames: tests/golden/frames/who22_sound_diffusion.yaml
  • Documentation:
    • Trace matrix cataloged in docs/trace-availability.md and README.md.

State Machine & Decision Diagram

Amplifier Lifecycle States

State Entity Status Hardware Stage Description
OFF off Powered Off Amplifier output stage is unpowered (*16*13*<WHERE>##). Zero bus audio is demodulated. Standby power is minimal.
WAKE_PENDING off / on Powering Up Integration initiates an OFF → ON wake sequence (*16*13*<WHERE>## followed by *16*3*<WHERE>##). A 3-second echo suppression window (WAKE_ECHO_WINDOW = 3.0s) ignores the initial OFF echo.
ON playing / on Active Amplifier is powered ON (*16*3*<WHERE>##). Demodulating stereo audio and driving room speakers.
PENDING_OFF paused / idle Active (Timer) Decoder has stopped or paused. A safety timer runs (3.0s for idle/off/standby, 60.0s for pause, or 5.0s group leave grace). If audio resumes before timer expiry, the timer cancels and playback continues without interruption.
PARKED paused / idle Powered Off Anti-hiss auto-off has powered off the hardware amplifier (*16*13*<WHERE>##) to eliminate analog bus hiss during extended pauses. In Home Assistant, the entity reports paused/idle and preserves group topology. Resumes immediately upon playback.

State Transition Diagram

stateDiagram-v2
    [*] --> OFF
    OFF --> WAKE_PENDING: play_media / turn_on
    WAKE_PENDING --> ON: *16*3*<WHERE>## confirmed
    OFF --> ON: Physical Vol-Up Rocker (*16*1001..1015*<WHERE>##)
    OFF --> ON: External Bus ON (*16*3*<WHERE>##)
    ON --> PENDING_OFF: Decoder Idle/Off (3s) or Paused (60s)
    ON --> PENDING_OFF: Member Unjoined (5s grace period)
    PENDING_OFF --> ON: Decoder resumes playing (Timer Cancelled)
    PENDING_OFF --> PARKED: Auto-Off Timer Expires (Group Preserved)
    PENDING_OFF --> OFF: Grace Period Expires (Standalone / Unjoined)
    PARKED --> ON: media_play / play_media / turn_on
    PARKED --> OFF: turn_off / Physical Wall Switch OFF (*16*13*<WHERE>##)
    ON --> OFF: turn_off / Physical Wall Switch OFF (*16*13*<WHERE>##)
Loading

Bus Frame Decision Engine

Frame Pattern Description Power Transition Handling & Rationale
*16*3*<WHERE>##
*16*0*<WHERE>##
Amplifier ON (stereo / mono) PowerTransition.WAKE_ON Powers the amplifier ON; cancels any pending auto-off or grace timers.
*16*1001*<WHERE>##
to *16*1015*<WHERE>##
Wall switch Volume-UP rocker (+1 to +15) PowerTransition.WAKE_ON Physical volume-up rocker intentionally wakes and turns ON the amplifier (#579).
*16*1101*<WHERE>##
to *16*1115*<WHERE>##
Wall switch Volume-DOWN rocker (−1 to −15) PowerTransition.NO_CHANGE Steps volume down only. Never powers on an inactive amplifier.
*#16*<WHERE>*1*<LEVEL>##
*#22*3#A#P*1*<LEVEL>##
Dimension 1 volume status report (0–31) PowerTransition.NO_CHANGE CRITICAL: Updates volume_level only. Never wakes or turns on an OFF amplifier (#669, #682).
*#16*<WHERE>*#1*<LEVEL>## Dimension #1 volume set command PowerTransition.NO_CHANGE Writes target volume level. Does not alter amplifier power state.
*16*3*1ES## Matrix environment routing frame PowerTransition.NO_CHANGE Routes environment E to source S. Handled by routing logic; does not change power state.
*16*3*10S## Source device activation frame PowerTransition.NO_CHANGE Toggles source hardware S. Does not alter room amplifier power.
*16*13*<WHERE>##
*16*10*<WHERE>##
Amplifier OFF (stereo / mono) PowerTransition.WAKE_ECHO_OFF
PowerTransition.PARKED_CONFIRM_OFF
PowerTransition.REAL_OFF
• If within 3.0s of wake: ignored as self-echo.
• If amplifier is parked: acknowledged as auto-off echo.
• Otherwise: real OFF from wall switch or gateway; powers off entity and disbands group.

…ound system state machine, and add intercom ducking trace replay (OpenWebNet-HA#682)
@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!

…ower transitions, support string dimensions, and add wake-window ducking tests
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 386df80 into OpenWebNet-HA:v2-phase1-architecture Oct 10, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the fix/issue-682-media-player-state-complexity branch October 21, 2026 19:35
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.

2 participants