Skip to content

fix(resync): require all registered area entities to echo before skipping or cancelling area sweep (#746) - #749

Merged
GreenGrassBlueOcean merged 2 commits into
v2-phase1-architecturefrom
fix/issue-746-area-resync-switch-04
Oct 17, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
v2-phase1-architecturefrom
fix/issue-746-area-resync-switch-04

Conversation

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

What is the problem

In Issue #746, @wave68runner reported that switch.luce_specchio_bagno (WHERE=04, Area "00", PL=04, configured on a BTicino MH200N gateway as a WHO=1 switch) updates normally when toggled directly from the wall button (*1*1*04## / *1*0*04##) or from Home Assistant, but remains stuck ON in Home Assistant when turned off via the physical General OFF wall button.

Analyzing the 117-frame OpenWebNet trace attached to #746 (myhome_trace_MH200N_all_2026-10-17T14-28-20.json) reveals the exact root cause:

  1. When the physical General OFF button is pressed (*15*01*81##), MH200N executes Scenario 6 (*17*1*6## -> *17*2*6##).
  2. Scenario 6 emits both the broadcast General OFF frame (*1*0*0##) and individual point-to-point commands for a subset of actuators in Area "00" (01, 03, 09, 0011), without emitting a dedicated point-to-point frame for 04 (because 04 is already turned off by the *1*0*0## broadcast).
  3. As of #732 (bus-truthful state), MyHOMELight and MyHOMESwitch entities intentionally ignore broadcast frames (WHERE="0" and area WHERE="00", "1".."9", "100") and rely on LightingResyncManager (custom_components/myhome/gateway_resync.py) to poll *#1*<area>## after the 0.5 s debounce window.
  4. However, LightingResyncManager treated any single point-to-point echo in an area (any(a == raw_where for _, _, a in self._recent_ptp) for leading echoes, or the first trailing echo in handle_ptp_echo) as proof that the entire area had already reported its state:
    • Because 01, 03, 09, and 0011 echoed in Area "00" during Scenario 6, LightingResyncManager skipped/cancelled *#1*00## while still polling *#1*1## through *#1*5##.
    • Consequently, MH200N was never asked *#1*00##, never emitted *1*0*04##, and switch 04 remained stuck ON in Home Assistant.

(Note: Open PR #740 partially touched _area_fully_echoed, but gated the subset check on if len(known) > 1: — which still fails when only 1 entity in Area "00" is registered in Home Assistant while unregistered siblings echo on the bus — and did not filter _recent_ptp by latest per-entity polarity, allowing a prior opposite-polarity transition such as turning 04 ON < 1.5 s before *1*0*0## to falsely satisfy _area_fully_echoed.)


How is it solved

  1. Per-entity area registry map (_known_area_map & known_area_entities):
    • LightingResyncManager now maps each area code ("00", "1".."9", "100") to the normalized set of WHERE addresses (normalize_where(address.clean_where)) registered under light or switch for the gateway's config_entry.
  2. Completeness check across all registered area entities (_area_fully_echoed):
    • Whenever known_area_entities(area) is non-empty (if known: return known.issubset(echoed_wheres)), an area resync sweep (*#1*<area>##) is only skipped (leading window) or cancelled (trailing debounce window) once every registered light/switch entity in that area has emitted a point-to-point echo.
    • Even if only a single entity (04) is registered in Area "00", echoes from unregistered actuators (01, 03, 09, 0011) cannot cancel *#1*00## until 04 itself echoes.
  3. Latest per-entity polarity matching (_ptp_polarities & _resync_area_echoes):
    • _leading_echoes_for_area() evaluates the latest point-to-point polarity recorded for each entity in _recent_ptp within RESYNC_LEADING_WINDOW_S (1.5 s) and only counts echoes whose polarity matches the broadcast (msg_is_on), preventing an ON transition (*1*1*04##) right before General OFF (*1*0*0##) from masquerading as a leading OFF echo.
    • Matching leading echoes seed self._resync_area_echoes[area], which then accumulates trailing echoes during the 0.5 s debounce window and cancels the timer only if _area_fully_echoed(area, echoed) becomes True.

How is it tested

  • Authentic MH200N trace replay (tests/test_issue_746_mh200n_gen_off_switch_trace.py):
    • Ingests @wave68runner's 117-frame MH200N capture (tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json).
    • Replays the wall switch turn-on (*1*1000#1*04## -> *1*1*04##) followed by the CEN pushbutton (*15*01*81##), MH200N Scenario 6 (*17*1*6## -> *17*2*6##), partial Area "00" echoes (01, 03, 09, 0011), and General OFF (*1*0*0##).
    • Verifies that Area "00" remains armed in _resync_timers, dispatches *#1*00## alongside *#1*1##..*#1*5##, and transitions switch_04 to OFF upon receiving *1*0*04##.
  • Unit & edge-case tests (tests/test_issue_368_resync.py):
    • test_partial_area_echoes_do_not_skip_or_cancel_area_sweep: verifies partial leading + trailing echoes in Area "00" (01, 03, 09, 0011 without 04) still emit *#1*00##, whereas full echoes (01, 03, 04, 09, 0011) cancel the timer without polling.
    • test_single_registered_entity_in_area_not_cancelled_by_unregistered_sibling_echo: verifies the len(known) == 1 edge case where only 04 is registered in Area "00" and unregistered 01 echoes.
    • test_opposite_polarity_leading_echo_does_not_skip_area_sweep: verifies turning 04 ON (*1*1*04##) within 1.5 s before General OFF (*1*0*0##) does not suppress *#1*00##.
  • Coverage & Standards:
    • 100.0% line coverage on custom_components/myhome/gateway_resync.py (162 Stmts, 0 Miss).
    • ruff check, scripts/verify_ha_standards.py, and scripts/sync_documentation.py --check all pass with zero warnings.

State Diagram

stateDiagram-v2
    [*] --> Idle

    Idle --> LeadingWindowCheck : RX Broadcast (*1*0*0## or *1*0*00##)
    state LeadingWindowCheck {
        [*] --> CollectLeadingEchoes
        CollectLeadingEchoes : Filter _recent_ptp (last 1.5s) for Area "00"\nusing latest polarity per entity == msg.is_on
        CollectLeadingEchoes --> CheckLeadingComplete : known_area_entities("00") = {"01","03","04","09","0011"}
    }

    CheckLeadingComplete --> Idle : known ⊆ leading_echoes\n(All entities already echoed -> skip sweep)
    CheckLeadingComplete --> DebounceArmed : known ⊈ leading_echoes\n(e.g. "04" missing -> seed _resync_area_echoes["00"])

    state DebounceArmed {
        [*] --> WaitingForTrailingEchoes : Arm 0.5s timer for Area "00"
        WaitingForTrailingEchoes --> AccumulateEcho : RX PTP in Area "00"\n(*1*0*01##, *1*0*03##, *1*0*09##, *1*0*0011##)
        AccumulateEcho --> WaitingForTrailingEchoes : echoed = {"01","03","09","0011"}\n("04" still missing -> keep timer armed)
    }

    DebounceArmed --> Idle : RX *1*0*04## before 0.5s\n(known ⊆ echoed -> cancel timer)
    DebounceArmed --> PollArea00 : 0.5s timer expires\n("04" never echoed)

    PollArea00 : TX *#1*00## (OWNLightingCommand.status("00"))
    PollArea00 --> BusStatusReply : MH200N queries SCS Area 00
    BusStatusReply : RX *1*0*04## from MH200N
    BusStatusReply --> SwitchUpdated : Router dispatches to MyHOMESwitch("04")\nswitch.luce_specchio_bagno -> OFF
    SwitchUpdated --> [*]
Loading

What does the Firmware Oracle / Encyclopedia say

1. Firmware Oracle (openwebnet-mcp — lookup_firmware_verdict & parse_and_validate_frame)

Querying the emulated firmware oracle for MH200N (010108, bt_fls) and comparing across the gateway matrix confirms:

Frame Purpose MH200N (010108) Verdict SCS Bus Telegram Emitted (OUT (Bus)) Cross-Gateway Support
*#1*00## Area 00 status request ACK (*#*1##) -> OUT (Bus) 24 30 34 42 33 30 30 31 35 30 30 0d ($04B3001500\r) ACK + Bus TX on MH200N, F453AV, F454, F459, F460, F461, H4684, MH202, MyHomeServer1 (NACKed only on legacy MH200 020102)
*#1*2## Area 2 status request ACK (*#*1##) -> OUT (Bus) 24 30 34 42 33 30 32 31 35 30 30 0d ($04B3021500\r) Identical $04B3<AA>1500\r SCS area-query telegram structure to *#1*00##
*#1*02## Point-to-point status request (A=00, PL=02) ACK (*#*1##) -> OUT (Bus) 24 30 34 42 30 30 32 31 35 30 30 0d ($04B0021500\r) ACK + Bus TX on all gateways (A=00, PL=01–09 uses 2-digit 01–09 addressing)
*1*1*02## / *1*0*02## Point-to-point ON / OFF (A=00, PL=02) ACK (*#*1##) -> OUT (Bus) 24 30 34 42 30 30 32 31 32 30 30 0d / ...31 0d Confirms WHERE=04 (A=00, PL=04) is a standard 2-digit Area 00 point-to-point address

2. OpenWebNet Encyclopedia Machine KB (WHO=1 Lighting & WHO=17 MH200N Scenarios)

  • Area 00 Addressing (WHO=1):
    • In WHO=1, Area 00 point-to-point addresses with PL 1..9 are encoded as two digits 01..09 (e.g. WHERE=04), and PL 10..15 are encoded as four digits 0010..0015 (e.g. WHERE=0011), while the Area 00 broadcast/status target is WHERE=00 (*#1*00##).
  • Silent Relay Actuation on General / Area Broadcasts (*1*0*0## / *1*0*<A>##):
    • Physical SCS relays actuated by a General (WHERE=0) or Area (WHERE=00, 1..9, 100) broadcast switch their load simultaneously without transmitting individual point-to-point *1*0*<WHERE>## status frames to avoid colliding on the 9600-baud SCS bus.
    • When an MH200N scenario (WHO=17, *17*1*6##) runs a mixed sequence containing both *1*0*0## and explicit point-to-point commands for specific actuators (01, 03, 09, 0011), only the explicitly addressed actuators emit point-to-point echoes; the remaining actuators in Area "00" (such as WHERE=04) only report their new OFF state when polled via *#1*00##.

Closes #746
Refs #748

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

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 8e426ac into v2-phase1-architecture Oct 17, 2026
17 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the fix/issue-746-area-resync-switch-04 branch October 17, 2026 18:40
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