Skip to content

feat(resync,light_group): scope group echo cancellation and multi-entity area sweeps (#734) - #740

Merged
GreenGrassBlueOcean merged 4 commits into
v2-phase1-architecturefrom
arch/issue-734-bus-truthful-state-and-resync
Oct 20, 2026
Merged

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

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Closes #734
Builds on #746 (area sweeps wait for every registered entity in the area), which is already on v2-phase1-architecture.

What is the problem

  1. Unscoped Group (#G) Echo Suppression in LightingResyncManager (custom_components/myhome/gateway_resync.py):
    • LightingResyncManager arms a debounced (RESYNC_DEBOUNCE_S = 0.5 s) *#1*#G## status poll after a group command (*1*1*#G## / *1*0*#G##), and skips or cancels it once the group's members have echoed their own point-to-point status within the 1.5 s leading window (RESYNC_LEADING_WINDOW_S) or 0.5 s trailing debounce window.
    • Before this PR, "the members have echoed" meant "any two WHO=1 point-to-point echoes":
      • an echo from an unrelated room or a different F422 bus interface (#4#<interface>) counted;
      • an echo with the opposite ON/OFF state counted, so a group OFF sent shortly after its members reported ON was skipped as already confirmed;
      • the same address echoing twice counted as two;
      • the members declared on a MyHOMELightGroup in myhome.yaml were never passed to LightingResyncManager, so it could not verify whether all declared members had echoed.
  2. Trailing Opposite-Polarity Echoes on Area / General Sweeps (custom_components/myhome/gateway_resync.py):
    • While _leading_echoes_for_area (Switch entities not updating after Gen OFF command from wall switch #746) checked that leading echoes matched the command's ON/OFF state when entities were registered in the area (known_area_entities(area)), the trailing handle_ptp_echo path did not check _resync_expected_on: an area ON (or general ON) followed by its registered light echoing OFF during the 0.5 s debounce window still cancelled *#1*<area>##.

How is it solved

  1. Group Member Registration, Interface Preservation & Polarity-Scoped Echo Tracking (custom_components/myhome/gateway_resync.py, custom_components/myhome/light_group.py):
    • LightingResyncManager.register_group_members(group_where, members) records the normalized point-to-point member WHERE addresses of a group (#G or #G#4#<interface> → set of point WHEREs via _normalize_group_key and _normalize_ptp_where, preserving explicit #4#<interface> suffixes and inheriting the group's #4#<interface> when bare PointToPoint members like ["11", "12"] are declared on a routed group in myhome.yaml). Passing an empty list (register_group_members(group_where, [])) removes the entry.
    • MyHOMELightGroup preserves any #4#<interface> suffix from device_id on _interface and _full_where, qualifies bare member WHEREs with #4#<interface> when resolving {mac}-1-{where} in the entity registry, registers its declared members in async_added_to_hass(), unregisters them via async_on_remove(lambda: register_fn(self._full_where, [])), and filters assumed-state group frames in handle_event() by _interface.
    • Group echoes are scoped across the 1.5 s leading window and 0.5 s trailing debounce window (with partial leading matches seeded into _resync_echoed_members via group_seed_echoes, matching area_seed_echoes):
      • the echo must match the group command's expected ON/OFF state (_resync_expected_on); a trailing opposite-polarity echo discards that member from _resync_echoed_members if it had echoed earlier in the window;
      • when members are registered for the group, the echo must belong to _group_members[group_key] (including matching #4#<interface>) and every declared member must echo before *#1*#G## is skipped or cancelled;
      • when a group has no declared members (assumed-state groups or bus-only groups not in myhome.yaml), the fallback requires 2 distinct point-to-point addresses on the same bus interface (#4#<interface> vs local bus) echoing with matching ON/OFF state;
      • echoes are counted per distinct address in _resync_echoed_members (mirrored to _resync_group_echoes).
  2. Trailing Polarity Enforcement on Area / General Sweeps (custom_components/myhome/gateway_resync.py):
    • schedule_resync() now records _resync_expected_on[where] for area targets (from both area and general commands) as well as group targets.
    • When an area has registered light / switch entities (known_area_entities(area)), handle_ptp_echo() requires trailing echoes to match _resync_expected_on[area] before adding them to _resync_area_echoes[area] and checking _area_fully_echoed(area, echoed). A trailing opposite-polarity echo discards any seeded echo for that address from _resync_area_echoes[area].
    • All tracking dictionaries (_resync_timers, _resync_group_echoes, _resync_area_echoes, _resync_echoed_members, _resync_expected_on) are cleaned up on cancellation, in execute_resync(), and in cancel_all().

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: with #6 = {11, 12}, neither leading OFF echoes of its members, nor leading/trailing ON echoes from 21 / 22, nor a member echoing ON then flipping OFF before the other member echoes ON can skip or cancel *#1*#6##. Also verifies split leading + trailing member completion (11 leading ON + 12 trailing ON) and that duplicate leading + trailing echoes of 11 count only once. Clearing members via register_group_members("6", []) removes the entry.
  • test_opposite_polarity_trailing_echo_does_not_cancel_area_sweep: verifies that an Area 1 ON command (*1*1*1##) is not cancelled when its registered light ({MAC}-1-11) echoes OFF (*1*0*11##) during the debounce window, nor when a seeded leading ON echo on 11 flips OFF before 12 echoes ON.
  • test_group_resync_preserves_bus_interface_on_members_and_group_key: verifies that a private-bus member 11#4#01 is not satisfied by a local-bus 11 echo (leading or trailing), that a routed group #6#4#01 ignores local-bus echoes and polls *#1*#6#4#01##, and that MyHOMELightGroup on #6#4#01 preserves #4#01 on _full_where, inherits #4#01 for bare members=["11", "12"] in both group_members and entity registry lookup ({mac}-1-11#4#01), and ignores local-bus group frames in handle_event().
  • test_replay_f461_live_2026_10_18_run1_lights_group_and_general: replays the 28 WHO=1 frames of run1_lights_group_and_general from a physical BTicino F461 (2.0.11, BT-F411/4 channels 06 and 07 registered as {mac}-1-06 and {mac}-1-07) at their captured timestamps on a virtual clock and virtual async_call_later:
    • with members=["06", "07"] and with members=[] (unscoped fallback): when *1*0*#1## arrives at t=9.447 s, the ON echoes of 06 (8.786 s) and 07 (8.898 s) inside the 1.5 s leading window have opposite polarity, so the #1 OFF timer is armed (whereas pre-feat(resync,light_group): scope group echo cancellation and multi-entity area sweeps (#734) #740 logic falsely skipped it) and then cancelled by the trailing OFF echoes at 9.505 s / 9.555 s; the two general commands (10.183 s, 12.461 s) dispatch *#1*00## at +0.5 s because F461's general echoes arrive at +0.73 s and +1.51 s;
    • with members=["06", "07", "08"] (where declared member 08 never echoes in the trace): 06 and 07 alone do not cancel #1, so *#1*#1## is dispatched at 9.238 s and 9.947 s in addition to the two *#1*00## sweeps.
  • test_multi_entity_area_sweep_waits_for_all_known_entities: guards multi-entity area completion (Switch entities not updating after Gen OFF command from wall switch #746).
  • 100.0% statement coverage across gateway_resync.py and light_group.py (and 100.0% across the full integration, 4336 passed), mypy --strict, ruff check, verify_ha_standards.py, and sync_documentation.py --check all green.

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 in the 1.5 s leading window
    Triggered --> Debouncing : otherwise, seed partial leading matches and arm the 0.5 s timer
    Debouncing --> Debouncing : echo from non-member / wrong bus interface, or opposite state (discards prior echo)
    Debouncing --> Debouncing : member echo with matching state (recorded)
    Debouncing --> Cancelled : last required member echoes with matching state
    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 area (A) WHERE carries no member list in the frame; actuator membership is stored in the physical actuators' configuration. Requiring all declared members (or all registered area entities) to echo with matching ON/OFF state and bus interface ensures MyHOME only suppresses *#1*#G## or *#1*A## when every known member has actually reported.

@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 force-pushed the arch/issue-730-native-fades-and-cover-strategies branch from d919be7 to c630e76 Compare October 17, 2026 10:15
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch 2 times, most recently from 1aedd67 to 7eb1348 Compare October 17, 2026 10:40
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-730-native-fades-and-cover-strategies branch from c630e76 to 37df2a8 Compare October 17, 2026 10:40
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-730-native-fades-and-cover-strategies branch 4 times, most recently from 2be0103 to 587efb1 Compare October 19, 2026 09:41
GreenGrassBlueOcean added a commit that referenced this pull request Oct 19, 2026
…larity and require all area entities to echo (#734, #740)
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from 7eb1348 to 348d40b Compare October 19, 2026 09:49
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-730-native-fades-and-cover-strategies branch from 587efb1 to 58ffd33 Compare October 19, 2026 12:33
…larity and require all area entities to echo (#734, #740)
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from 348d40b to e5ea6cd Compare October 19, 2026 12:37
Base automatically changed from arch/issue-730-native-fades-and-cover-strategies to v2-phase1-architecture October 20, 2026 13:28
GreenGrassBlueOcean and others added 3 commits October 20, 2026 20:30
…, #740)

The replay fed all 28 frames back to back, so the whole trace sat in one
1.5 s leading window and the test passed on the pre-#740 logic as well.
It now drives a virtual clock and a virtual async_call_later from the
frame timestamps.

On that timeline the *1*0*#1## OFF sweep is armed rather than skipped
(06/07 echoed ON 0.55-0.66 s earlier, wrong polarity) and the member
echoes cancel both group #1 sweeps. Both *#1*00## sweeps after the
general commands go out: the F461 echoes 06 and 07 0.7 s and 1.5 s after
*1*1*0## / *1*0*0##, past the 0.5 s debounce. The old assertion that
they were cancelled only held because the replay ignored time.

Also registers the members with the {mac}-1-{where} unique_id format
light_group uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto arch/issue-734-bus-truthful-state-and-resync
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 60255fb into v2-phase1-architecture Oct 20, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the arch/issue-734-bus-truthful-state-and-resync branch October 20, 2026 19:06
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/MyHOME that referenced this pull request Oct 20, 2026
GreenGrassBlueOcean added a commit that referenced this pull request Oct 21, 2026
…l-state-and-resync

fix(resync,light_group): inherit group bus interface for members and seed leading group echoes (#734, #740)
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