Repository navigation
fix(resync,light_group): inherit group bus interface for members and seed leading group echoes (#734, #740) - #771
Merged
GreenGrassBlueOcean merged 1 commit intoOct 21, 2026
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
GreenGrassBlueOcean
deleted the
arch/issue-734-bus-truthful-state-and-resync
branch
October 21, 2026 23:11
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.
Follow-up to #740 (#734): lands the post-merge commit
94f8ea5cpushed toarch/issue-734-bus-truthful-state-and-resyncright after #740 was merged.What is the problem
Partial Leading Group Echoes Discarded When Arming the
0.5 sDebounce Timer (custom_components/myhome/gateway_resync.py):*1*1*A##) arrives after some—but not yet all—known area entities echoed with matching ON/OFF polarity inside the1.5 sleading window (RESYNC_LEADING_WINDOW_S),schedule_resync()seeds those partial leading matches into_resync_area_echoes[where]viaarea_seed_echoes.*1*1*#G##/*1*0*#G##), however,schedule_resync()checked whether all required members had echoed in the1.5 sleading window and, if not, reset_resync_echoed_members[where] = set()and_resync_group_echoes[where] = 0when arming the0.5 sdebounce timer (RESYNC_DEBOUNCE_S).11echoed in the1.5 sleading window before*1*1*#6##and member12echoed in the0.5 strailing debounce window after*1*1*#6##,11's leading echo was forgotten and*#1*#6##still fired unnecessarily.Bare
PointToPointMembers on F422-Routed Groups Do Not Inherit#4#<interface>(custom_components/myhome/gateway_resync.py,custom_components/myhome/light_group.py):myhome.yaml, a group on an F422 private riser (device_id: "1-#6#4#1"or#6#4#01) can list barePointToPointmember addresses (members: ["11", "12"]).LightingResyncManager.register_group_members()normalized each member without inheriting the group key's#4#<interface>suffix (storing{"11", "12"}instead of{"11#4#01", "12#4#01"}), andMyHOMELightGroup.async_added_to_hass()looked up{mac}-1-11in the entity registry instead of{mac}-1-11#4#01.members=[]) routedMyHOMELightGroupon#7#4#01did not filter incoming group frames inhandle_event()by_interface, so a local-bus*1*1*#7##frame falsely mutated the riser group's state.How is it solved
Seed Partial Leading Group Echoes (
custom_components/myhome/gateway_resync.py):schedule_resync()records partial leading-window matches ingroup_seed_echoes[group_key] = matching_wheres(mirroringarea_seed_echoes) and seeds_resync_echoed_members[where] = set(group_seed_echoes.get(where, set()))and_resync_group_echoes[where] = len(seeded)when arming the0.5 stimer.0.5 sdebounce window,handle_ptp_echo()discards that member from_resync_echoed_members[where].Inherit
#4#<interface>for Routed Group Members & Filter Assumed-State Frames by Interface (custom_components/myhome/gateway_resync.py,custom_components/myhome/light_group.py):LightingResyncManager.register_group_members()extractsgrp_iface = key.partition(BUS_ROUTING)[2] or Nonefrom the normalized group key and passesgrp_ifaceto_normalize_ptp_where(m, grp_iface), so bare member addresses inherit the group's#4#<interface>when none is explicitly present on the member string.MyHOMELightGroup.__init__()normalizes and storesself._interface("01"for both1-#6#4#1and1-#6#4#01) and buildsself._full_wherefrom it.MyHOMELightGroup.async_added_to_hass()qualifies bare member WHEREs withf"{w}{BUS_ROUTING}{self._interface}"before resolving{mac}-1-{member_key}in the entity registry.MyHOMELightGroup.handle_event()normalizesgetattr(msg, "interface", None)(zero-padding numeric strings to 2 digits) and ignores group frames whose interface does not matchself._interface.Known limits (not changed here)
membersare not scoped to a room: two distinct same-interface echoes with matching ON/OFF state from any room still cancel the poll.06and07echo0.73 sand1.51 safter*1*1*0##/*1*0*0##, after the0.5 sRESYNC_DEBOUNCE_Stimer expires. That sends one*#1*00##per general command; changingRESYNC_DEBOUNCE_Sis out of scope for this PR.How is it tested
tests/test_issue_368_resync.py:test_group_resync_ignores_cross_room_and_opposite_polarity_echoes:11leading ON +12trailing ON cancels#6viagroup_seed_echoes).11) is followed by a trailing opposite-polarity echo (*1*0*11##),resync_group_echoes["#6"]drops from1to0,12echoing ON alone does not cancel#6, and11re-echoing ON completes the group and cancels the timer.test_group_resync_preserves_bus_interface_on_members_and_group_key:MyHOMELightGroupondevice_id="1-#6#4#1"with baremembers=["11", "12"]normalizes_full_where == "#6#4#01", registersmgr.group_members["#6#4#01"] == {"11#4#01", "12#4#01"}, and resolves_member_entity_idsfor{mac}-1-11#4#01and{mac}-1-12#4#01.MyHOMELightGroup(device_id="1-#7#4#01",members=[]) ignores local-bus*1*1*#7##(is_on is None) and accepts*1*1*#7#4#01##(is_on is True).test_replay_f461_live_2026_10_18_run1_lights_group_and_general:declared = ["06", "07"]anddeclared = [](2-distinct-address same-polarity fallback) across the 28-frame physicalBTicino F461(2.0.11) capture.gateway_resync.pyandlight_group.py,mypy --strict,ruff check, andscripts/verify_ha_standards.pypass.State diagram
stateDiagram-v2 [*] --> Idle state "Group / Area command seen (*1*1*#G##, *1*0*#G##, *1*1*A##, *1*0*A##)" as Triggered state "Collecting member echoes (0.5 s RESYNC_DEBOUNCE_S)" as Debouncing state "Poll skipped / cancelled" as Cancelled state "Poll sent (*#1*#G## / *#1*A##)" as Dispatched Idle --> Triggered Triggered --> Cancelled : every required member echoed with matching state & interface in 1.5 s leading window Triggered --> Debouncing : otherwise, seed partial leading matches (group_seed_echoes / area_seed_echoes) and arm 0.5 s timer Debouncing --> Debouncing : echo from non-member / wrong #4#interface (ignored) Debouncing --> Debouncing : echo with opposite state (discards prior seeded or trailing echo for that member) Debouncing --> Debouncing : member echo with matching state & interface (recorded) Debouncing --> Cancelled : last required member echoes with matching state & interface Debouncing --> Dispatched : 0.5 s timer expires first Cancelled --> Idle Dispatched --> IdleWhere are the traces
tests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json, runrun1_lights_group_and_general: physicalBTicino F461fw2.0.11,BT-F411/4at06/07, group#1commands with Dimension 15 group status (*#1*#1*15*1##/*#1*#1*15*0##), and general commands*1*1*0##/*1*0*0##.What do the Oracle / Encyclopedia say
*1*1*#1##and*#1*#1##have not yet been replayed intests/golden/firmware_oracle.json; gateway behaviour for group frames is taken from the live physicalF461capture.WHO=1): a group (#Gor#G#4#I) WHERE carries no member list on the wire; actuator membership is stored in the physical actuators' configuration on that bus segment (#4#I). Qualifying bare group members with the group's#4#<interface>suffix and seeding partial leading echoes ensuresMyHOMEtracks group completion accurately per bus interface.