Skip to content

fix(events): fire scope events only for frames that carry a state (#367) - #797

Open
gdluck wants to merge 6 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/events-state-frames
Open

gdluck wants to merge 6 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/events-state-frames

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

Two scope events still fire without a state behind them, the remaining part of #367 (the group/area on/off path was fixed earlier), and the review of this PR (5639737594) found three more places where a WHAT 30/31 dim step is taken for an "on":

  • A WHAT 30/31 relative dim step to a group, area or general address (*1*30*#5##, *1*31*3##, *1*30#2#100*#5##) parses with is_on True in OWNd 2.0.0b11, so myhome_group_light_event / myhome_area_light_event / myhome_general_light_event fired with event: "on", an assumed-state MyHOMELightGroup flipped to on, a point-to-point MyHOMELight did the same at *1*31*12##, and the member re-sync recorded expected_on = True, so the members' *1*0*<ptp>## echoes (dimmed down to 0 %) were discarded as the wrong polarity and *#1*#G## / *#1*A## went out although every member had spoken. The frame only says the lights moved one step; from 0 % they may still be off.
  • A WHO 2 DIMENSION 10 status readback on a group, area or general address (*#2*#5*10*10*50*0*0##, *#2*0*10*10*50*0*0##: shutterStatus 10, stationary at 50 %) parses with is_opening False, is_closing False, so the automation event fired with event: "stop" and, on WHERE 0, ran the gateway's centralized_shutter_stop device trigger when nothing had stopped.

An automation that mirrors a wall group onto an HA group therefore switched it on at every dim step and saw stops that never happened. Refs #367 (closed).

The what in (30, 31) checks in MyHOME are a bridge: the right place for "a dim step has no on/off state" is OWNd, where OWNLightingEvent.is_on would return None for WHAT 30/31. Resync eligibility no longer hangs on is_on is not None, so when that OWNd change lands the member re-sync keeps running and only the bridge checks become dead code. That OWNd follow-up is named here, not opened.

Based on the current v2-phase1-architecture (d8bac15, #799 and #801 in-tree), no conflict.

What is the solution

  • gateway_events.py, WHO 1 branch: a dim step (what 30/31) fires no on/off event; the group/area/general member re-sync runs whether OWNd reports the step's is_on as True or None. A point-to-point dim step (*1*31*12##) is no longer a member echo.
  • gateway_resync.py: a dim step is swept with _resync_expected_on[target] = None, so both ON and OFF member echoes count towards cancelling the sweep; schedule_resync accepts what in (30, 31) regardless of is_on.
  • light_group.py (assumed-state group) and light.py (point-to-point light): handle_event skips what in (30, 31); the entity keeps its last known state.
  • gateway_events.py, WHO 2 branch: the same is_not_dimension guard as WHO 1, so a DIMENSION 10 status readback never fires a scope automation event; a WHAT outside the table (is_opening / is_closing None) states nothing either. WHAT 0/1/2 and the advanced WHAT 10 (stop) / 11 (up) / 12 (down) fire open / close / stop as before.
  • Docs: covers.md gets a "Bus Events for Scope Commands" section (addresses, payload, which frames fire and which do not, the centralized shutter triggers, an example); lights.md keeps its three light events and links across. The release-notes delta names all three light events and uses *#2*#5*10*10*50*0*0## as the WHO 2 example.

Companion PR: none (the OWNd-side is_on = None for WHAT 30/31 is a named follow-up, not opened).

State Diagram

stateDiagram-v2
    direction TB

    state "1. WHO 1 scope frame (group #G, area A, general 0)" as Who1 {
        [*] --> OnOff : *1*1*#6## / *1*0*#6##\nis_on True / False
        [*] --> DimStep : *1*30*#5## / *1*31*3## / *1*30#2#100*#5##\nrelative step, WHAT 30/31 - OWNd is_on True today, None later
        OnOff --> LightEvent : myhome_group / area / general_light_event on / off\nthen member resync with expected polarity
        DimStep --> LightEvent : before - fired as on, assumed group and ptp light flipped on
        DimStep --> ResyncOnly : after - no event, no entity flip,\nmember resync with expected polarity None - ON and OFF echoes cancel
    }

    state "2. WHO 2 scope frame" as Who2 {
        [*] --> Command : *2*0*0## / *2*1*3## / *2*2*#5##\nadvanced *2*10#001#1*0## stop, *2*11#100#001#1*0## up, *2*12#100#001#1*0## down
        [*] --> Dimension : *#2*#5*10*10*50*0*0## / *#2*0*10*10*50*0*0##\nDIMENSION 10 status readback, stationary
        Command --> AutomationEvent : myhome_group / area / general_automation_event\nopen / close / stop, unchanged - general drives centralized_shutter_* triggers
        Dimension --> AutomationEvent : before - fired as stop
        Dimension --> NoEvent : after - nothing fired
    }
Loading

Hardware Verification

Live F461 2.0.11 plant (local Home Assistant, combined build of all our PRs, 2026-10-24) and, for status reads, the MyHomeServer1 3.87.13 plant; captures in #799 (merged).

Live retest (2026-10-24)

Consensus label: confirmed for the tested case (combined build, F461 2.0.11, HA 2026.10.2). myhome.send_message *2*0*0## while the cover was opening → tx *2*0*0##, rx *2*0*0## and exactly one myhome_general_automation_event {"message": "*2*0*0##", "event": "stop", "where": "0"}; *1*1*#1## → one myhome_group_light_event on, *1*0*0## → one myhome_general_light_event off, and none for the resync reads *#1*#1## / *#1*00##. The dim-step and WHO 2 dimension cases were not exercised live (no such device on the plant); the advanced WHO 2 scope commands come from the issue_445 captures below.

How is it tested

Every finding of the review has its own commit, each with tests that fail before it and pass after it (plain upstream OWNd master f04d8aa on PYTHONPATH, HA 2026.10.2, Python 3.14):

  • tests/test_gateway.py::test_scope_events_fire_only_for_state_frames: fifteen real frames through listening_loop (WHO 1 dim steps plain / parameterised / general, two DIMENSION 10 scope readbacks, one malformed DIMENSION 10 frame OWNd hands through unparsed, the issue_445 advanced general stop / up / down, the issue_645 *2*0*100##, and on/off, stop/open frames). The exact fired list is pinned: eight events, none for the dim steps, the readbacks or the unparsed frame; _resync_timers holds #5, #6, 3 with _resync_expected_on == {"#5": None, "3": None, "#6": False}. On the base branch it fails with six extra events.
  • tests/test_issue_368_resync.py: test_dim_step_sweeps_members_with_unknown_polarity (*1*31*#5## + two OFF echoes, *1*30*#6## + one ON and one OFF echo, *1*31*3## + one OFF echo all cancel their sweep; silence still sends *#1*#7##), test_dim_step_resync_does_not_depend_on_is_on (a WHAT 31 frame with is_on None still schedules), test_point_to_point_dim_steps_are_not_member_echoes (ptp dim steps neither seed nor cancel #6).
  • tests/test_issue_368_groups.py::test_dim_step_does_not_flip_assumed_group, tests/test_component_light.py::test_dim_step_does_not_flip_point_to_point_light (x3 frames): unknown / off / on survive *1*30*…##, *1*31*…##, *1*30#2#100*…##.
  • tests/test_device_trigger.py::test_centralized_shutter_stop_ignores_dimension_10_status: through a real MyHOMEGatewayHandler, *#2*0*10*10*50*0*0## / *#2*#5*10*10*50*0*0## do not run the centralized_shutter_stop trigger; *2*0*0## and *2*10#001#1*0## do.
  • Fail-before counts per commit: cdd2f4b 3 tests, a0314fc 5, 2387478 2, 69c9a54 test-only (fails on base with six extra events), 5c2ccc0 docs.
  • Full suite, -n 4: 4507 passed, 1 skipped (base d8bac15, same setup: 4498 passed, 1 skipped).
  • scripts/verify_ownd_coverage.py: 100 % statement coverage (16,075 / 16,075). ruff check ., scripts/typing_ratchet.py (mypy --strict, 0 errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check (links and anchors validated) pass.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 2 general stop #799 (merged), tests/fixtures/traces/f461_live_2026-10-23/…json, run6_event_session_ha_fixed_integration; tests/test_f461_live_2026_10_23_trace.py::test_general_stop_was_seen_exactly_once BTicino F461 2.0.11 *2*0*0## appears exactly once (one general stop from Home Assistant); a command frame, so it fires exactly one myhome_general_automation_event stop before and after this PR.
WHO 2 advanced scope commands tests/fixtures/traces/issue_445/ (LN4660M2 centralized button, 2026-09-26 / 09-28) BTicino MyHomeServer1 *2*10#001#1*0## (advanced stop), *2*11#100#001#1*0## (advanced up), *2*12#100#001#1*0## (advanced down) on WHERE 0: command frames, fire general stop / open / close in the pinned test; the same captures carry the point-to-point DIMENSION 10 status polls (*#2*<ptp>*10*…##) that this PR keeps away from the scope events.
WHO 2 area stop tests/fixtures/traces/issue_645/ (2026-10-11) BTicino MyHomeServer1 *2*0*100##: a command frame to area 10, fires myhome_area_automation_event stop.
WHO 1 dim steps and WHO 2 DIMENSION 10 readbacks on scope addresses No physical trace exists: the plants have no WHO 1 group or area dimmer and no WHO 2 group, and no community capture in tests/fixtures/traces/ carries *1*30*#G##, *1*31*A## or *#2*<scope>*10*…##. The tests feed those frames through OWNMessage.parse and the gateway's listening_loop, so their is_on / is_opening / is_closing / dimension values are OWNd's own (parser truth), not mocked. none none captured; parser-truth only, stated as such

What does the Firmware Oracle / Encyclopedia say

No oracle stimulus exists for the frames of this PR (results/<P>/<V>/oracle/full/*.tsv has no *2*0*0##, *1*30*#5##, *1*31*3## or WHO 2 dimension frame to a scope address): the oracle records what a gateway does with a request; this PR changes what MyHOME fires when such a frame is heard.

The Encyclopedia settles the protocol facts the PR relies on:

  • WHO 1 relative dim steps: ownkb:claim:c001418, ownkb:claim:c001419, ownkb:claim:c001421 (functional/who-1-lighting/what.md): WHAT 30 / 31 and 30#LEVELS#SPEED / 31#LEVELS#SPEED are relative step commands, not ON/OFF states, which is why they fire no light event and sweep their members with an unknown polarity.
  • WHO 2 command WHAT table: ownkb:claim:c002649 and ownkb:chunk:r000468 (functional/who-2-automation/what.md): the advanced commands are WHAT 10 (Advanced Stop), 11 (Advanced Up), 12 (Advanced Down); the diagram and the test use exactly these.
  • WHO 2 dimensions: ownkb:claim:c002578 / ownkb:chunk:r000453 (functional/who-2-automation/dimensions.md): WHO 2 has only DIMENSION 10 (shutter status) and the write-only DIMENSION 11 (go to level). The test therefore uses DIMENSION 10 status readbacks and one malformed DIMENSION 10 frame, no other dimension.

Review follow-up (review 5639737594)

  1. LightingResyncManager polarity on WHAT 30/31 and the is_on is not None coupling: cdd2f4b (gateway_events.py, gateway_resync.py, tests in test_issue_368_resync.py, test_gateway.py).
  2. Assumed-state MyHOMELightGroup, point-to-point MyHOMELight and handle_ptp_echo flipping on at WHAT 30/31: a0314fc (light_group.py, light.py, gateway_events.py, tests in test_issue_368_groups.py, test_component_light.py, test_issue_368_resync.py).
  3. WHO 2 DIMENSION 10 scope readbacks firing stop / centralized_shutter_stop: 2387478 (gateway_events.py, tests in test_gateway.py, test_device_trigger.py).
  4. Test frames (undocumented WHO 2 dimensions 12/13, read-reply on write-only 11): 69c9a54 (test_gateway.py, real frames from the Encyclopedia tables and the issue_445 / issue_645 captures).
  5. Docs and release note (WHO 2 scope events in covers.md, myhome_general_light_event, valid DIMENSION 10 example): 5c2ccc0. Encyclopedia citations, #799 (merged) and the physical WHO 2 scope captures: this description.

Checklist

  • PR Description: Follows the structured sections above (What is the problem, What is the solution, State Diagram, How is it tested, Where are the traces, What does the Firmware Oracle / Encyclopedia say).
  • Trace-Backed (if protocol/device change): the general stop is in the test(trace): ingest live F461 2.0.11 2026-10-23 capture and MyHomeServer1 3.87.13 reads behind OWNd #136-#142 and MyHOME #791-#798 #799 fixture and was seen live; the advanced WHO 2 scope commands and the area stop are in the issue_445 / issue_645 captures; the dim-step and DIMENSION 10 scope readback frames are parser-truth only, stated as such above.
  • V2 Architecture: the dispatcher and the resync manager are the gateway's own; the two entity changes guard handle_event only, no new platform code.
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

🤖 Generated with Claude Code

@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 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes before undrafting/merging. Suppressing false on / stop scope bus events for #367 is the right direction, and CI is green on f555c2db, but a few behavioural, protocol, and test issues need to be addressed first:

  1. LightingResyncManager polarity bug on WHAT 30/31 + broken OWNd follow-up path (gateway_events.py, gateway_resync.py):
    • Today OWNLightingEvent.is_on is True on WHAT 30/31, so _schedule_resync(message) records _resync_expected_on[target] = True (gateway_resync.py:251-291). When a WHAT 31 dim-down step dims members to 0% (*1*0*<ptp>##), handle_ptp_echo and leading-echo filtering discard valid OFF member echoes (False != True), preventing sweep cancellation and forcing unnecessary *#1*#G## / *#1*A## polls.
    • Tomorrow, if OWNd makes OWNLightingEvent.is_on return None on WHAT 30/31 as proposed in the PR body, _schedule_resync(message) will silently stop running altogether because both gateway_events.py:309 and gateway_resync.py:252 gate resync on is_on is not None.
  2. Assumed-state MyHOMELightGroup (and point-to-point WHAT 30/31) still flip on (light_group.py, light.py, gateway_events.py):
    • MyHOMELightGroup.handle_event (light_group.py:195-199) still reads msg.is_on is True and flips an off assumed-state group entity to on on *1*31*#5## (and handle_ptp_echo / MyHOMELight.handle_event treat point-to-point *1*31*12## as is_on = True).
  3. WHO 2 DIMENSION 10 scope status readbacks still fire stop events & centralized_shutter_stop triggers (gateway_events.py):
    • Unlike WHO 1 (is_not_dimension), the WHO 2 branch does not exclude dimension frames, so a stationary DIMENSION 10 status frame on a scope address (*#2*#5*10*10*50*0*0## / *#2*0*10*10*50*0*0##) has is_opening=False, is_closing=False and still fires event: "stop" (contradicting docs/configuration/lights.md and firing device_trigger.py's centralized_shutter_stop on WHERE=0).
  4. Protocol / Encyclopedia & PR description alignment:
    • Encyclopedia citations (## What does the Firmware Oracle / Encyclopedia say): Please cite the relevant Machine KB records rather than leaving the section empty:
      • WHO 1 relative dim steps: ownkb:claim:c001418, c001419, c001421 (functional/who-1-lighting/what.md — WHAT 30/31 and 30#LEVELS#SPEED / 31#LEVELS#SPEED are relative step commands, not ON/OFF states).
      • WHO 2 command WHAT table: ownkb:claim:c002649 & ownkb:chunk:r000468 (functional/who-2-automation/what.md — advanced command WHATs are 10 (Advanced Stop), 11 (Advanced Up), 12 (Advanced Down); the PR body currently omits WHAT 10 while listing 13 and 14, which are DIMENSION 10 shutter-status values rather than command WHATs).
      • WHO 2 dimensions: ownkb:claim:c002578 / ownkb:chunk:r000453 (functional/who-2-automation/dimensions.md — only DIMENSION 10 and write-only DIMENSION 11 exist for WHO 2; DIMENSION 12/13 in the test/diagram are undocumented).
    • Traces & rebase (## Where are the traces): Update #799 (open) -> #799 (merged), rebase onto current v2-phase1-architecture so #799's trace is in-tree (f555c2db is parented on 974306b5), and note existing physical WHO 2 scope captures in tests/fixtures/traces/issue_445/ (*2*10#001#1*0##, *2*11#100#001#1*0##, *2*12#100#001#1*0##) and tests/fixtures/traces/issue_645/ (*2*0*100##).

Inline comments below cover the specific changes in each file.

Comment on lines +307 to +312
is_dim_step = getattr(message, "what", None) in (30, 31)

if message.is_on is not None and is_not_dimension:
event = "on" if message.is_on else "off"
if message.is_general:
if is_dim_step:
pass # no on/off event; the members are still resynced below

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues in the WHO 1 dim-step handling and one entity-level gap:

  1. False expected_on = True polarity in LightingResyncManager today:
    In OWNd==2.0.0b11, OWNLightingEvent("*1*31*#5##").is_on is True. Passing message into self.handler._schedule_resync(message) at line 341 stores self._resync_expected_on[target] = True (gateway_resync.py:251-291). When a WHAT 31 dim-down step dims member actuators down to 0%, their spontaneous *1*0*<ptp>## echoes (msg_on = False) fail msg_on != expected_on in both leading-echo filtering (gateway_resync.py:269, 290) and handle_ptp_echo (gateway_resync.py:157-178), discarding valid OFF echoes and forcing redundant *#1*#G## / *#1*A## polls. For WHAT 30/31, target polarity is unknown (_resync_expected_on[target] should be None) so both ON and OFF member echoes count toward cancelling the sweep.

  2. OWNd follow-up would silently disable member re-sync:
    The PR body notes that once OWNd returns is_on = None for WHAT 30/31, "MyHOME's existing is_on is not None guard would do the rest ... once it lands the bridge can go." Because _schedule_resync(message) is nested inside if message.is_on is not None and is_not_dimension: here and LightingResyncManager.schedule_resync (gateway_resync.py:252) returns early when msg_is_on is None, changing OWNd to is_on = None would silently stop member re-sync from running on group/area/general dim steps. Please decouple resync eligibility from is_on is not None in both gateway_events.py and gateway_resync.py now ((message.is_on is not None or is_dim_step) and is_not_dimension, and allow what in (30, 31) in schedule_resync).

  3. Assumed-state MyHOMELightGroup (and point-to-point WHAT 30/31) still flip on:
    Messages are dispatched to entities at line 266 before reaching this block. On a group configured without members: (assumed_state: true), MyHOMELightGroup.handle_event (light_group.py:195-199) still checks if msg.is_on is True: self._attr_is_on = True, so *1*31*#5## still flips an off group entity to on. Likewise, point-to-point *1*31*12## is still treated as is_on = True in handle_ptp_echo (line 295) and MyHOMELight.handle_event (light.py:1058). Please guard what not in (30, 31) in those paths until OWNd handles WHAT 30/31 directly.

Comment on lines +342 to +348
elif (
isinstance(message, OWNAutomationEvent)
and self._is_active_for_who(2)
# Only a frame that states the motion is an open/close/stop event: a
# dimension frame with no known state is none of them (#367).
and (message.is_opening is not None or message.is_closing is not None)
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing is_not_dimension symmetry with WHO 1:

In OWNd, a stationary WHO 2 DIMENSION 10 status frame on a scope address (e.g. *#2*#5*10*10*50*0*0## or *#2*0*10*10*50*0*0##, shutterStatus = 10 at 50%) parses with is_opening = False, is_closing = False. Without is_not_dimension, that status readback enters the else: event = "stop" branch and fires myhome_{group,area,general}_automation_event with event: "stop" — which on WHERE=0 also triggers the gateway's centralized_shutter_stop device trigger (device_trigger.py:262-283) when nothing was stopped, and contradicts docs/configuration/lights.md:98 ("which fire for open / close / stop frames only").

Please add the same is_not_dimension guard used on WHO 1 (not (isinstance(dim, int) and not isinstance(dim, bool))) so dimension status readbacks never fire command scope events.

Comment thread tests/test_gateway.py
Comment on lines +525 to +557
frames = [
"*1*30*#5##", # group 5 dim up one step
"*1*31*3##", # area 3 dim down one step
"*#2*#5*11*50##", # WHO 2 dimension 11 to group 5
"*#2*3*12*1##", # WHO 2 dimension 12 to area 3
"*#2*0*13*1##", # WHO 2 dimension 13, general
"*1*1*#6##", # group 6 on: a real state, still fired
"*1*0*#6##", # group 6 off
"*2*0*#5##", # group 5 stop: a real state
"*2*1*3##", # area 3 open
]
with patch("custom_components.myhome.gateway.OWNEventSession") as mock_session_class:
mock_session = MagicMock()
mock_session.connect = AsyncMock(return_value={"Success": True})
mock_session.get_next = AsyncMock(side_effect=[OWNMessage.parse(f) for f in frames] + [asyncio.CancelledError()])
mock_session_class.return_value = mock_session
gateway_handler.send_status_request = AsyncMock()
try:
await gateway_handler.listening_loop()
except asyncio.CancelledError:
pass

fired = [
(call.args[0], call.args[1].get("message"), call.args[1].get("event"))
for call in gateway_handler.hass.bus.async_fire.call_args_list
if call.args[0].endswith(("_light_event", "_automation_event"))
]
assert fired == [
("myhome_group_light_event", "*1*1*#6##", "on"),
("myhome_group_light_event", "*1*0*#6##", "off"),
("myhome_group_automation_event", "*2*0*#5##", "stop"),
("myhome_area_automation_event", "*2*1*3##", "open"),
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two test-quality gaps here:

  1. Use real WHO 2 protocol frames instead of non-existent dimensions 12 / 13:
    Per the OpenWebNet Encyclopedia (ownkb:claim:c002578, ownkb:chunk:r000453), WHO 2 defines only DIMENSION 10 (Shutter status) and DIMENSION 11 (Go to level, write-only). *#2*3*12*1## (DIMENSION 12) and *#2*0*13*1## (DIMENSION 13) are undocumented WHO 2 dimensions, and *#2*#5*11*50## is read-reply syntax on write-only DIMENSION 11 (an actual write *#2*#5*#11#001*50## parses as OWNAutomationCommand). Please test real WHO 2 frames: stationary DIMENSION 10 status frames (*#2*#5*10*10*50*0*0##, *#2*0*10*10*50*0*0##), an unparsed/malformed dimension frame, advanced WHO 2 scope commands (*2*10#001#1*0##, *2*11#100#001#1*0## from tests/fixtures/traces/issue_445/), and parameterized/general WHO 1 dim steps (*1*30#2#100*#5##, *1*30*0##).

  2. Assert member re-sync scheduling and OFF echo cancellation on WHAT 31:
    gateway_handler.send_status_request is mocked on line 541 and the commit message notes "the member re-sync still runs", yet nothing asserts that _resync_manager._resync_timers was scheduled for #5 and 3, or that OFF member echoes (*1*0*<ptp>##) after a WHAT 31 dim-down step cancel the sweep cleanly.

Comment thread docs/configuration/lights.md Outdated

### Synchronizing Physical Wall Switches (SCS Group Buttons)
If you have physical BTicino wall switches configured to trigger an SCS group (e.g. `WHERE = #1`), the integration automatically dispatches a bus event named `myhome_group_light_event` whenever group frames are intercepted on the SCS bus. You can keep your Home Assistant Light Group perfectly synchronized with a simple automation:
If you have physical BTicino wall switches configured to trigger an SCS group (e.g. `WHERE = #1`), the integration automatically dispatches a bus event named `myhome_group_light_event` whenever a group frame with a state (`on` / `off`) is intercepted on the SCS bus. A dim step to the group (`*1*30*#1##` / `*1*31*#1##`) fires no event, as it does not say whether the group is on; the same holds for the area and general events and for the WHO 2 automation events (`myhome_group_automation_event`, `myhome_area_automation_event`, `myhome_general_automation_event`), which fire for `open` / `close` / `stop` frames only. You can keep your Home Assistant Light Group perfectly synchronized with a simple automation:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documenting WHO 2 shutter scope events (myhome_group_automation_event, myhome_area_automation_event, myhome_general_automation_event) solely inside a single long sentence under Lights & Dimmers (WHO = 1) makes them hard to find for cover users (docs/configuration/covers.md currently has no mention of them). Please document the WHO 2 automation scope events in docs/configuration/covers.md as well (keeping the lighting paragraph focused or cross-linked).

Comment thread .github/release-notes/2.0.0b16.md Outdated
- **`myhome.send_message` and `myhome.sync_time` reject unknown keys.** A misspelt key such as `gw_mac` or a missing `message` raises a validation error instead of sending the frame to the default gateway; both services get text selectors in the visual editor (#189).
- **Climate zones show the setpoint in force (dimension 12).** When the thermostat regulates to another setpoint than the commanded one (dimension 14; 23.5 in force while 21.0 commanded on an F461), the entity and `local_target_temperature` now show the one in force instead of the commanded value plus the knob offset. Known limit: once a zone has reported dimension 12, a dimension 14 that is not the echo of a Home Assistant write does not move the slider until the next dimension 12 report or poll (see Known Limitations).
- **`myhome.sync_time` refuses time zones with a half-hour offset** (e.g. `Asia/Kolkata`, UTC+5:30) with a translated error. The WHO 13 time-zone field carries whole hours only; b15 (on OWNd 2.0.0b10) set the gateway clock with a truncated offset without saying so (#764).
- **Group, area and general bus events only for frames that carry a state.** A dim step (`*1*30*#1##` / `*1*31*#1##`) no longer fires `myhome_group_light_event` / `myhome_area_light_event` as `on`, and a WHO 2 dimension frame to a group, area or general address (`*#2*#5*11*50##`) no longer fires an automation event as `stop`; on/off, open/close/stop frames fire as before (#367).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullet title covers Group, area and general bus events and gateway_events.py gates myhome_general_light_event (*1*30*0## / *1*31*0##) too, but the text here only lists myhome_group_light_event / myhome_area_light_event. Please include myhome_general_light_event and replace the synthetic *#2*#5*11*50## example with a valid WHO 2 DIMENSION 10 frame (e.g. *#2*#5*10*10*50*0*0##).

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All five points are on the branch (head 5c2ccc0, one commit per finding, still draft, on the current base). A second-opinion review of the diff found no remaining correctness issue; its one question (what a point-to-point WHAT 30/31 frame does after the new guard in MyHOMELight.handle_event) was checked: OWNd gives unknown_state, brightness, brightness_preset and the colour fields as None for *1*31*12## / *1*30#2#100*12##, so every later branch is skipped and the test asserts unknown_state stays unset.

  1. Resync polarity (cdd2f4b): WHAT 30/31 is swept with _resync_expected_on[target] = None, so both *1*1*<ptp>## and *1*0*<ptp>## member echoes cancel the sweep; eligibility no longer depends on is_on is not None, neither in gateway_events.py ((is_on is not None or is_dim_step) and is_not_dimension) nor in LightingResyncManager.schedule_resync (what in (30, 31) accepted), so an OWNd that returns is_on = None for the dim steps keeps the member re-sync running. Tests: *1*31*#5## / *1*31*3## schedule #5 and 3 with no expected polarity; OFF and mixed echoes after a dim-down cancel the sweep; a WHAT 31 with is_on None still schedules.
  2. Entity flips (a0314fc): MyHOMELightGroup.handle_event (assumed state), MyHOMELight.handle_event and the handle_ptp_echo gate skip what in (30, 31); *1*31*#5## / *1*31*12## switch nothing on and point-to-point dim steps neither seed nor cancel a sweep. Tests across unknown / off / on for *1*30*…##, *1*31*…##, *1*30#2#100*…##.
  3. WHO 2 dimension guard (2387478): the WHO 2 branch has the same is_not_dimension guard as WHO 1; *#2*#5*10*10*50*0*0## and *#2*0*10*10*50*0*0## fire no stop and no centralized_shutter_stop (tested through a real handler with the device trigger attached); *2*0*0## and *2*10#001#1*0## still do.
  4. Real frames only (69c9a54): dimensions 12/13 and the read-reply *#2*#5*11*50## are gone; the scope test drives DIMENSION 10 readbacks, one malformed DIMENSION 10 frame (unparsed, handed through as the raw string), the issue 445 advanced scope commands (*2*10#001#1*0##, *2*11#100#001#1*0##, *2*12#100#001#1*0##), issue 645 *2*0*100##, and *1*30#2#100*#5## / *1*30*0##; the fired list and the resync timers/polarities are pinned.
  5. Docs and description (5c2ccc0 + body): the WHO 2 scope events have their own section in covers.md (cross-linked from lights.md); the release note names myhome_general_light_event and uses a valid DIMENSION 10 frame; the Encyclopedia citations are in as you listed them (c001418 / c001419 / c001421; c002649 / r000468 with WHAT 10 stop, 11 up, 12 down; c002578 / r000453); #799 (merged) plus the issue 445 and 645 captures in the traces table; the diagram no longer shows dimensions 12/13.

Full suite 4507 passed (base 4498), 100 % coverage, ruff / mypy-strict / HA standards / docs sync clean.

gdluck and others added 6 commits October 24, 2026 23:52
…enWebNet-HA#367)

A WHAT 30/31 dim step to a group, area or general address parses with
`is_on True`, so `myhome_{group,area,general}_light_event` fired as
"on" although the frame only says the light changed one step. A WHO 2
dimension frame to a scope address (`*#2*michnovka#5*11*50##`, `*#2*3*12*1##`,
`*#2*0*13*1##`) parses with `is_opening` / `is_closing` None, so the
automation event fired as "stop" with no state behind it.

Dim steps now fire no light event (the member re-sync still runs) and
the WHO 2 branch fires only when the frame states a motion. On a
physical F461 2.0.11, `*2*0*0##` (general stop) fires exactly one
`myhome_general_automation_event` with `event: stop`, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…enWebNet-HA#367)

OWNd 2.0.0b11 parses `*1*31*michnovka#5##` with `is_on True`, so the member
re-sync recorded `expected_on = True` for the group. When the step dims
the members down to 0 % their `*1*0*<ptp>##` echoes were discarded as
the wrong polarity, the sweep could not be cancelled and `*#1*#G##` /
`*#1*A##` went out although every member had spoken.

A dim step commands no polarity: `_resync_expected_on[target]` is now
`None` for WHAT 30/31 and both ON and OFF member echoes count towards
cancelling the sweep. Resync eligibility no longer hangs on
`is_on is not None` in the dispatcher or in
`LightingResyncManager.schedule_resync`, so an OWNd that reports
`is_on None` for a dim step keeps the member re-sync running instead of
silently disabling it.

Tests: `*1*31*michnovka#5##` / `*1*31*3##` schedule the `michnovka#5` and `3` sweeps with
no expected polarity; OFF and mixed member echoes after a dim step
cancel them; a WHAT 31 frame with `is_on None` still schedules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ebNet-HA#367)

OWNd 2.0.0b11 reports a dim step as `is_on True`, so an assumed-state
`MyHOMELightGroup` flipped to on at `*1*31*michnovka#5##`, a point-to-point
`MyHOMELight` did the same at `*1*31*12##`, and the dispatcher counted
the point-to-point step as an ON member echo of a resync sweep. A step
is a relative command: from 0 % it may well leave the light off.

`MyHOMELightGroup.handle_event`, `MyHOMELight.handle_event` and the
member-echo path in the dispatcher now skip `what in (30, 31)` until
OWNd stops calling a step "on"; the entity keeps its last known state
and a step neither seeds nor cancels a sweep.

Tests: an assumed-state group and a dimmer stay unknown / off / on
across `*1*30*…##`, `*1*31*…##` and `*1*30#2#100*…##`; point-to-point
dim steps around `*1*1*michnovka#6##` leave its sweep armed until the members'
own on/off echoes arrive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…OpenWebNet-HA#367)

The WHO 2 branch had no `is_not_dimension` guard like WHO 1, so a
stationary DIMENSION 10 status readback on a scope address
(`*#2*michnovka#5*10*10*50*0*0##`, `*#2*0*10*10*50*0*0##`, shutterStatus 10 at
50 %) parsed with `is_opening False, is_closing False` and fired
`myhome_{group,area,general}_automation_event` with `event: stop`; on
WHERE 0 that also ran the gateway's `centralized_shutter_stop` device
trigger when nothing had stopped.

A dimension frame is never an open/close/stop event; a WHAT outside the
table (`is_opening` / `is_closing` None) states nothing either. Command
frames `*2*0*0##`, `*2*10#001#1*0##` (advanced stop) fire as before.

Tests: the two DIMENSION 10 scope readbacks fire nothing through the
dispatcher, and the `centralized_shutter_stop` trigger attached to a
real handler fires for `*2*0*0##` and `*2*10#001#1*0##` only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…OpenWebNet-HA#367)

The test fed `*#2*3*12*1##` and `*#2*0*13*1##` (WHO 2 has no DIMENSION
12 or 13) and `*#2*michnovka#5*11*50##` (read-reply syntax on the write-only
DIMENSION 11). It now uses the frames a plant produces: DIMENSION 10
status readbacks on a group and the general address, one malformed
DIMENSION 10 frame OWNd hands through unparsed, the advanced scope
commands captured in `tests/fixtures/traces/issue_445/`
(`*2*10#001#1*0##`, `*2*11#100#001#1*0##`, `*2*12#100#001#1*0##`) and
`issue_645/` (`*2*0*100##`), and the parameterised and general WHO 1
dim steps `*1*30#2#100*michnovka#5##` / `*1*30*0##`.

The exact fired list is pinned: the advanced commands fire general
stop / open / close, the area stop and the on/off and open/stop frames
fire as before, and nothing fires for the dim steps, the dimension
readbacks and the unparsed frame. On the base branch the same test
fails with six extra events.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three `myhome_{general,area,group}_automation_event` events were
described only inside one long sentence of the lighting page, where a
cover user does not look. The cover docs now have a "Bus Events for
Scope Commands" section: addresses and payload, the command frames that
fire (`WHAT 0/1/2` and the advanced `WHAT 10/11/12` from the issue_445
captures), the frames that do not (DIMENSION 10 status readbacks,
dimension writes, a WHAT outside the table), the link to the gateway's
centralized shutter triggers and an example automation. The lighting
paragraph keeps its own three events and links across.

Release note: names `myhome_general_light_event` with the other two,
mentions the entity and re-sync fixes, and uses a valid DIMENSION 10
frame (`*#2*michnovka#5*10*10*50*0*0##`) as the WHO 2 example.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/events-state-frames branch from 5c2ccc0 to 99a12b3 Compare October 24, 2026 21:10
@gdluck
gdluck marked this pull request as ready for review October 24, 2026 22:08
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All 17 checks green on 99a12b3 (review points above addressed); marking ready for review.

This branch has not been deployed

No deployments
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.

3 participants