Repository navigation
fix(resync): require all registered area entities to echo before skipping or cancelling area sweep (#746) - #749
Merged
GreenGrassBlueOcean merged 2 commits intoOct 17, 2026
Conversation
…ping or cancelling area sweep (#746)
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This was referenced Oct 21, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the problem
In Issue #746,
@wave68runnerreported thatswitch.luce_specchio_bagno(WHERE=04, Area"00",PL=04, configured on a BTicinoMH200Ngateway as aWHO=1switch) updates normally when toggled directly from the wall button (*1*1*04##/*1*0*04##) or from Home Assistant, but remains stuckONin 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:*15*01*81##),MH200Nexecutes Scenario 6 (*17*1*6##->*17*2*6##).*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 for04(because04is already turned off by the*1*0*0##broadcast).#732(bus-truthful state),MyHOMELightandMyHOMESwitchentities intentionally ignore broadcast frames (WHERE="0"and areaWHERE="00","1".."9","100") and rely onLightingResyncManager(custom_components/myhome/gateway_resync.py) to poll*#1*<area>##after the0.5 sdebounce window.LightingResyncManagertreated 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 inhandle_ptp_echo) as proof that the entire area had already reported its state:01,03,09, and0011echoed in Area"00"during Scenario 6,LightingResyncManagerskipped/cancelled*#1*00##while still polling*#1*1##through*#1*5##.MH200Nwas never asked*#1*00##, never emitted*1*0*04##, andswitch04remained stuckONin Home Assistant.(Note: Open PR #740 partially touched
_area_fully_echoed, but gated the subset check onif 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_ptpby latest per-entity polarity, allowing a prior opposite-polarity transition such as turning04ON< 1.5 sbefore*1*0*0##to falsely satisfy_area_fully_echoed.)How is it solved
_known_area_map&known_area_entities):LightingResyncManagernow maps each area code ("00","1".."9","100") to the normalized set ofWHEREaddresses (normalize_where(address.clean_where)) registered underlightorswitchfor the gateway'sconfig_entry._area_fully_echoed):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 registeredlight/switchentity in that area has emitted a point-to-point echo.04) is registered in Area"00", echoes from unregistered actuators (01,03,09,0011) cannot cancel*#1*00##until04itself echoes._ptp_polarities&_resync_area_echoes):_leading_echoes_for_area()evaluates the latest point-to-point polarity recorded for each entity in_recent_ptpwithinRESYNC_LEADING_WINDOW_S(1.5 s) and only counts echoes whose polarity matches the broadcast (msg_is_on), preventing anONtransition (*1*1*04##) right before GeneralOFF(*1*0*0##) from masquerading as a leadingOFFecho.self._resync_area_echoes[area], which then accumulates trailing echoes during the0.5 sdebounce window and cancels the timer only if_area_fully_echoed(area, echoed)becomesTrue.How is it tested
MH200Ntrace replay (tests/test_issue_746_mh200n_gen_off_switch_trace.py):@wave68runner's 117-frameMH200Ncapture (tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json).*1*1000#1*04##->*1*1*04##) followed by the CEN pushbutton (*15*01*81##),MH200NScenario 6 (*17*1*6##->*17*2*6##), partial Area"00"echoes (01,03,09,0011), and General OFF (*1*0*0##)."00"remains armed in_resync_timers, dispatches*#1*00##alongside*#1*1##..*#1*5##, and transitionsswitch_04toOFFupon receiving*1*0*04##.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,0011without04) 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 thelen(known) == 1edge case where only04is registered in Area"00"and unregistered01echoes.test_opposite_polarity_leading_echo_does_not_skip_area_sweep: verifies turning04ON(*1*1*04##) within1.5 sbefore GeneralOFF(*1*0*0##) does not suppress*#1*00##.100.0%line coverage oncustom_components/myhome/gateway_resync.py(162 Stmts, 0 Miss).ruff check,scripts/verify_ha_standards.py, andscripts/sync_documentation.py --checkall 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 --> [*]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:MH200N(010108) VerdictOUT (Bus))*#1*00##00status requestACK (*#*1##)->OUT (Bus)24 30 34 42 33 30 30 31 35 30 30 0d($04B3001500\r)MH200N,F453AV,F454,F459,F460,F461,H4684,MH202,MyHomeServer1(NACKed only on legacyMH200020102)*#1*2##2status requestACK (*#*1##)->OUT (Bus)24 30 34 42 33 30 32 31 35 30 30 0d($04B3021500\r)$04B3<AA>1500\rSCS area-query telegram structure to*#1*00##*#1*02##A=00, PL=02)ACK (*#*1##)->OUT (Bus)24 30 34 42 30 30 32 31 35 30 30 0d($04B0021500\r)A=00,PL=01–09uses 2-digit01–09addressing)*1*1*02##/*1*0*02##A=00, PL=02)ACK (*#*1##)->OUT (Bus)24 30 34 42 30 30 32 31 32 30 30 0d/...31 0dWHERE=04(A=00,PL=04) is a standard 2-digit Area00point-to-point address2. OpenWebNet Encyclopedia Machine KB (
WHO=1Lighting &WHO=17MH200N Scenarios)00Addressing (WHO=1):WHO=1, Area00point-to-point addresses withPL1..9are encoded as two digits01..09(e.g.WHERE=04), andPL10..15are encoded as four digits0010..0015(e.g.WHERE=0011), while the Area00broadcast/status target isWHERE=00(*#1*00##).*1*0*0##/*1*0*<A>##):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.MH200Nscenario (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 asWHERE=04) only report their newOFFstate when polled via*#1*00##.Closes #746
Refs #748