Skip to content

fix(resync,light_group): inherit group bus interface for members and seed leading group echoes (#734, #740) - #771

Merged
GreenGrassBlueOcean merged 1 commit into
v2-phase1-architecturefrom
arch/issue-734-bus-truthful-state-and-resync
Oct 21, 2026
Merged

GreenGrassBlueOcean merged 1 commit into
v2-phase1-architecturefrom
arch/issue-734-bus-truthful-state-and-resync

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #740 (#734): lands the post-merge commit 94f8ea5c pushed to arch/issue-734-bus-truthful-state-and-resync right after #740 was merged.

What is the problem

  1. Partial Leading Group Echoes Discarded When Arming the 0.5 s Debounce Timer (custom_components/myhome/gateway_resync.py):

    • When an area command (*1*1*A##) arrives after some—but not yet all—known area entities echoed with matching ON/OFF polarity inside the 1.5 s leading window (RESYNC_LEADING_WINDOW_S), schedule_resync() seeds those partial leading matches into _resync_area_echoes[where] via area_seed_echoes.
    • For group commands (*1*1*#G## / *1*0*#G##), however, schedule_resync() checked whether all required members had echoed in the 1.5 s leading window and, if not, reset _resync_echoed_members[where] = set() and _resync_group_echoes[where] = 0 when arming the 0.5 s debounce timer (RESYNC_DEBOUNCE_S).
    • If member 11 echoed in the 1.5 s leading window before *1*1*#6## and member 12 echoed in the 0.5 s trailing debounce window after *1*1*#6##, 11's leading echo was forgotten and *#1*#6## still fired unnecessarily.
  2. Bare PointToPoint Members on F422-Routed Groups Do Not Inherit #4#<interface> (custom_components/myhome/gateway_resync.py, custom_components/myhome/light_group.py):

    • In myhome.yaml, a group on an F422 private riser (device_id: "1-#6#4#1" or #6#4#01) can list bare PointToPoint member 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"}), and MyHOMELightGroup.async_added_to_hass() looked up {mac}-1-11 in the entity registry instead of {mac}-1-11#4#01.
    • Additionally, an assumed-state (members=[]) routed MyHOMELightGroup on #7#4#01 did not filter incoming group frames in handle_event() by _interface, so a local-bus *1*1*#7## frame falsely mutated the riser group's state.

How is it solved

  1. Seed Partial Leading Group Echoes (custom_components/myhome/gateway_resync.py):

    • schedule_resync() records partial leading-window matches in group_seed_echoes[group_key] = matching_wheres (mirroring area_seed_echoes) and seeds _resync_echoed_members[where] = set(group_seed_echoes.get(where, set())) and _resync_group_echoes[where] = len(seeded) when arming the 0.5 s timer.
    • As with trailing echoes, if a seeded member subsequently emits an opposite-polarity echo during the 0.5 s debounce window, handle_ptp_echo() discards that member from _resync_echoed_members[where].
  2. 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() extracts grp_iface = key.partition(BUS_ROUTING)[2] or None from the normalized group key and passes grp_iface to _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 stores self._interface ("01" for both 1-#6#4#1 and 1-#6#4#01) and builds self._full_where from it.
    • MyHOMELightGroup.async_added_to_hass() qualifies bare member WHEREs with f"{w}{BUS_ROUTING}{self._interface}" before resolving {mac}-1-{member_key} in the entity registry.
    • MyHOMELightGroup.handle_event() normalizes getattr(msg, "interface", None) (zero-padding numeric strings to 2 digits) and ignores group frames whose interface does not match self._interface.

Known limits (not changed here)

  • Groups without declared members are not scoped to a room: two distinct same-interface echoes with matching ON/OFF state from any room still cancel the poll.
  • On the F461, the polls after general commands always go out: in the F461 trace below, 06 and 07 echo 0.73 s and 1.51 s after *1*1*0## / *1*0*0##, after the 0.5 s RESYNC_DEBOUNCE_S timer expires. That sends one *#1*00## per general command; changing RESYNC_DEBOUNCE_S is 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:
    • Verifies split leading + trailing member completion (11 leading ON + 12 trailing ON cancels #6 via group_seed_echoes).
    • Verifies that when a seeded leading ON echo (11) is followed by a trailing opposite-polarity echo (*1*0*11##), resync_group_echoes["#6"] drops from 1 to 0, 12 echoing ON alone does not cancel #6, and 11 re-echoing ON completes the group and cancels the timer.
  • test_group_resync_preserves_bus_interface_on_members_and_group_key:
    • Verifies MyHOMELightGroup on device_id="1-#6#4#1" with bare members=["11", "12"] normalizes _full_where == "#6#4#01", registers mgr.group_members["#6#4#01"] == {"11#4#01", "12#4#01"}, and resolves _member_entity_ids for {mac}-1-11#4#01 and {mac}-1-12#4#01.
    • Verifies an assumed-state routed 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:
    • Exercises both declared = ["06", "07"] and declared = [] (2-distinct-address same-polarity fallback) across the 28-frame physical BTicino F461 (2.0.11) capture.
  • 100.0% statement coverage on gateway_resync.py and light_group.py, mypy --strict, ruff check, and scripts/verify_ha_standards.py pass.

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 --> Idle
Loading

Where are the traces

What do the Oracle / Encyclopedia say

  • Firmware oracle: *1*1*#1## and *#1*#1## have not yet been replayed in tests/golden/firmware_oracle.json; gateway behaviour for group frames is taken from the live physical F461 capture.
  • OpenWebNet Encyclopedia (WHO=1): a group (#G or #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 ensures MyHOME tracks group completion accurately per bus interface.

@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 e160715 into v2-phase1-architecture Oct 21, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the arch/issue-734-bus-truthful-state-and-resync branch October 21, 2026 23:11
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