Skip to content

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

Merged
GreenGrassBlueOcean merged 9 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/events-state-frames
Oct 25, 2026
Merged

GreenGrassBlueOcean merged 9 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*0##, *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 frame on a group, area or general address (*#2*#5*10*10*50*001*0##, *#2*0*10*10*50*001*0##) 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. A physical shutter never sends such a frame: it reports DIMENSION 10 on its own point-to-point address (one *#2*<APL>*10*…## per actuator after a scope command, tests/fixtures/traces/issue_445/), so this is a defensive parser guard, symmetrical with the WHO 1 dimension guard, for a grammatically valid frame that only a third-party sender or a synthetic bus frame would put on the event session.

An automation that mirrors a wall group onto an HA group therefore switched it on at every dim step. 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 (089fef5, #812 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, as a defensive parser invariant: a WHO 2 dimension frame on a scope address never fires a scope automation event, and a WHAT outside the table (is_opening / is_closing None, e.g. *2*7*0##) states nothing either. WHAT 0/1/2 and the advanced WHAT 10 (stop) / 11 (up) / 12 (down) fire open / close / stop as before. The DIMENSION 11 level echo a gateway really puts on the event session for a scope level write (*#2*#5*#11#001#1*50##) is parsed by OWNd as an OWNAutomationCommand and never reached this branch, before or after: it is pinned as a regression control, not as a behaviour change.
  • 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 presents the WHO 2 change as the defensive guard it is.

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*0## / *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\nlive F461 - the points re-report *1*0*06## etc. as point events only
    }

    state "2. WHO 2 scope frame" as Who2 {
        [*] --> Command : *2*0*0## / *2*1*3## / *2*2*#5## / *2*0*100##\nadvanced *2*10#001#1*0## stop, *2*11#100#001#1*0## up, *2*12#100#001#1*0## down
        [*] --> Dimension : *#2*#5*#11#001#1*50## level echo (OWNAutomationCommand)\n*#2*#5*10*10*50*001*0## scope DIMENSION 10 - defensive guard,\nnot a readback a shutter sends (OWNAutomationEvent)
        Command --> AutomationEvent : myhome_group / area / general_automation_event\nopen / close / stop, unchanged - general drives centralized_shutter_* triggers
        Dimension --> AutomationEvent : before - scope DIMENSION 10 fired as stop
        Dimension --> NoEvent : after - no dimension frame fires anything
    }
Loading

Hardware Verification

Live F461 2.0.11 plant (local Home Assistant, combined build of all our PRs, 2026-10-24; plain OWNd master f04d8aa as sender on 2026-10-25) and, for status reads, the MyHomeServer1 3.87.13 plant; captures in #799 (merged) and the 2026-10-25 capture on file.

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 advanced WHO 2 scope commands come from the issue_445 captures below.

Live dim-step check (2026-10-25, F461 2.0.11, plain OWNd master as sender, event session recorded)

  • *1*30*0## (general dim up one level; the plant has no dimmer): acknowledged in 69 ms, echoed on the event session as *1*30*0##, and then every point re-reported its own state, twice over about 2 s: *1*0*06##, *1*0*07##, *1*0*09##, *2*0*05##.
  • *1*31*#1## (group 1 dim down one level): acknowledged in 62 ms, echoed as *1*31*#1##, followed by *1*0*06## only.

This is the "live *1*30*0## check" asked for in review 5642831869: a populated bus acknowledges and echoes the scope dim step even without a group dimmer, and the per-point reports that follow it are exactly the frames this PR must keep as point events. tests/test_gateway.py::test_general_dim_step_point_reports_fire_no_scope_events replays the sequence. The capture is on file and will be ingested as a fixture by a later trace PR (the WHO 2 dimension guard was not exercised live: a physical shutter does not send a scope DIMENSION 10 frame, see below).

How is it tested

Every finding of the reviews 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: eighteen real frames through listening_loop (WHO 1 dim steps plain / parameterised / general; the two DIMENSION 11 scope level echoes *#2*#5*#11#001#1*50## / *#2*0*#11#001#1*50##, pinned as OWNAutomationCommand dimension 11; the two scope DIMENSION 10 frames in the gateway's 3-digit priority format *#2*#5*10*10*50*001*0## / *#2*0*10*10*50*001*0##, pinned as OWNAutomationEvent dimension 10; one malformed DIMENSION 10 frame OWNd hands through unparsed; one WHAT outside the table *2*7*0## pinned as is_opening / is_closing None; the issue_445 advanced general stop / up / down; the area 10 stop *2*0*100##; and on/off, stop/open frames). The exact fired list is pinned: eight events, none for the dim steps, the dimension frames, the unparsed frame or the unknown WHAT; _resync_timers holds #5, #6, 3 with _resync_expected_on == {"#5": None, "3": None, "#6": False}. On the base branch it fails with seven extra events (four dim steps, two scope DIMENSION 10 frames and the unknown WHAT, all as on / stop).
  • tests/test_gateway.py::test_general_dim_step_point_reports_fire_no_scope_events: the live 2026-10-25 sequence above (eleven frames) through listening_loop; pins that no scope light or automation event fires, that every frame reaches the point-to-point dispatch once in bus order, that both dim steps reach _schedule_resync, and that the leading point reports (06, 07, 09) satisfy the group 1 sweep before it is armed. On the base branch it fails with two extra events (myhome_general_light_event and myhome_group_light_event as on).
  • 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*001*0## / *#2*#5*10*10*50*001*0## and the DIMENSION 11 echoes *#2*0*#11#001#1*50## / *#2*#5*#11#001#1*50## do not run the centralized_shutter_stop trigger; *2*0*0## and *2*10#001#1*0## do.
  • Fail-before counts per commit: 6469570 3 tests, dfc1034 5, 0e9ad84 2, a494138 test-only (fails on base with six extra events), 828468b docs; 9ebde27 citation only, 9d6547e 1 test (fails on base with two extra events), ef3417d wording + frames (the guard is unchanged; with the new frames the scope test fails on base with seven extra events instead of six, the trigger test still with the two scope DIMENSION 10 frames).
  • Full suite, -n 4: 4569 passed, 1 skipped (base 089fef5, same setup: 4559 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), scripts/manage_translations.py check 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 per-actuator DIMENSION 10 reports (*#2*<APL>*10*…##, one per shutter, never on a scope address) that this PR keeps away from the scope events.
WHO 2 area stop none captured. tests/fixtures/traces/issue_645/ is not a capture of it: its burst trace reconstructed ten redacted point-to-point status requests as WHERE 91..100, so its *#2*100## → *2*0*100## pair is a status reply on a reconstructed address (the earlier revision of this description cited it wrongly). — *2*0*100## is backed by the firmware oracle verdict below (reviewer's QEMU replay): a command frame to area 10, fires myhome_area_automation_event stop.
WHO 1 dim steps on scope addresses 2026-10-25 capture on file (to be ingested by a later trace PR), see "Live dim-step check" BTicino F461 2.0.11, plain OWNd master as sender *1*30*0## and *1*31*#1## acknowledged and echoed on the event session, followed by per-point state reports; replayed by test_general_dim_step_point_reports_fire_no_scope_events. The group / area / parameterised variants (*1*30*#5##, *1*31*3##, *1*30#2#100*#5##) are backed by the firmware oracle verdicts below.
WHO 2 dimension frames on scope addresses none, by nature: a physical shutter reports DIMENSION 10 on its own point-to-point address only (issue_445 above), so no capture carries *#2*<scope>*10*…##. The DIMENSION 11 scope level echoes *#2*#5*#11#001#1*50## / *#2*0*#11#001#1*50## and the 3-digit priority format of the DIMENSION 10 test frames come from the firmware oracle verdicts below. — the is_not_dimension guard is a defensive parser invariant; the test frames are what the gateway firmware emits (DIMENSION 11) or would parse (DIMENSION 10), fed through OWNMessage.parse and listening_loop.

What does the Firmware Oracle / Encyclopedia say

own-firmware-oracle tests both directions: down (OpenWebNet request → SCS bus frame + event-session echo) and up (inbound SCS telegram → event-session frame). results/<P>/<V>/**/*.tsv at 52d354b (main) holds no published row for *1*30*0##, *1*30*#5##, *1*31*3##, *1*30#2#100*#5##, *2*0*100##, *#2*#5*#11#001*50## or any WHO 2 scope dimension frame (checked by grep over all 636 TSVs). The verdicts below are therefore the reviewer's own QEMU replays, quoted from review 5642831869 (GreenGrassBlueOcean, 2026-10-25) against the real bt_luci / coso binaries, and are cited as such; a published suite would supersede this quote.

WHO 1 scope dim steps (reviewer's table):

Direction & stimulus MH200N 010108 F461 020011 / MyHomeServer1 028206
down *1*30*#5## ack, bus:$04B5051203, own:*1*30*#5## bus:$004B5051203 / $04B5051203
down *1*31*3## ack, bus:$04B3031204, own:*1*31*3## bus:$004B3031204 / $04B3031204
down *1*30*0## ack, bus:$04B1001203, own:*1*30*0## bus:$004B1001203 / $04B1001203
down *1*30#2#100*#5## ack, bus:$06D50501320D030264, own:*1*30#2#100*#5## bus:$006D50501420D030264 / $06D50501420D030264
up $17B5051203 (wall keypad → #5) — MyHomeServer1: own:*1*30*#5##
up $17B3031204 (wall keypad → 3) — MyHomeServer1: own:*1*31*3##
up $17B1001203 (wall keypad → 0) — MyHomeServer1: own:*1*30*0##
up $18D50501320D030264 (ext step → #5) — MyHomeServer1: own:*1*30#2#100*#5##

So a scope dim step is a real bus frame in both directions, a wall keypad produces exactly the frames this PR handles, and the live F461 capture above confirms the down row for *1*30*0## on a physical plant (ACK 69 ms, event-session echo, per-point re-reports). None of these frames carries an on/off state, which is why they fire no light event and sweep their members with an unknown polarity.

WHO 2 area stop (reviewer's replay on MyHomeServer1 028206): down *2*0*100## → bus:$04B30A120A (area 0x0A = 10, opcode 12 0A = stop) and own:*2*0*100## on the event session, whereas down *#2*100## emits $04B30A1500, to which actuators reply on their individual APL addresses, never with *2*0*100##. This is the citation for the *2*0*100## test frame.

WHO 2 scope dimension frames (reviewer's replay on coso, F461 020011 / MyHomeServer1 028206): the scope DIMENSION 11 level writes down *#2*#5*#11#001*50## / down *#2*0*#11#001*50## echo on the event session as *#2*#5*#11#001#1*50## / *#2*0*#11#001#1*50## (now in the tests; plain OWNd master parses them as OWNAutomationCommand, dimension 11). coso's inbound 0x44 0x09 decoder is address-mode generic (up $018D5050144090A3200 → own:*#2*#5*10*10*50*010*0##, 3-digit priority), so a scope DIMENSION 10 frame reaches the guard only from a synthetic or third-party bus frame; the test frames use that 3-digit priority (001).

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.
  • 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 and addressing: ownkb:claim:c002578 / ownkb:chunk:r000453, ownkb:chunk:r000458 (functional/who-2-automation/dimensions.md, addressing.md): WHO 2 has DIMENSION 10 (shutter status) and the write-only DIMENSION 11 (go to level); a scope request or command expands into one DIMENSION 10 frame per individual shutter (APL), which is why a scope DIMENSION 10 frame is a parser guard and not a readback.

Review follow-up (review 5639737594)

  1. LightingResyncManager polarity on WHAT 30/31 and the is_on is not None coupling: 6469570 (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: dfc1034 (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 frames firing stop / centralized_shutter_stop: 0e9ad84 (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): a494138 (test_gateway.py, real frames from the Encyclopedia tables and the issue_445 captures).
  5. Docs and release note (WHO 2 scope events in covers.md, myhome_general_light_event, valid DIMENSION 10 example): 828468b. Encyclopedia citations, #799 (merged) and the physical WHO 2 scope captures: this description.

Review follow-up (review 5642831869)

  1. *2*0*100## is not an issue_645 capture: 9ebde27 drops the issue_645/ citation from test_gateway.py (docstring and frame comment); this description's "Where are the traces" row says what the fixture really holds and cites the MyHomeServer1 028206 oracle replay (down *2*0*100## → bus:$04B30A120A, own:*2*0*100##) as the reviewer's QEMU verdict, no TSV row being published yet.
  2. WHO 1 dim steps are provable: 9d6547e adds test_general_dim_step_point_reports_fire_no_scope_events, the replay of the live F461 2.0.11 *1*30*0## / *1*31*#1## capture of 2026-10-25 (capture on file, fixture to follow in a trace PR); "What does the Firmware Oracle / Encyclopedia say" now states that the oracle tests both directions and quotes the reviewer's down/up table; every "parser truth only" wording is gone from this description (none was in the tree).
  3. WHO 2 scope DIMENSION 10 is a defensive parser guard: ef3417d rewords the gateway_events.py comment, the covers.md bullets and the b16 release note (no "the shutter is stationary at 50 %" on a scope address; the DIMENSION 11 echo is named as what a gateway really emits and as a frame that never fired), switches the test frames to the 3-digit priority (*#2*#5*10*10*50*001*0##, *#2*0*10*10*50*001*0##) in test_gateway.py and test_device_trigger.py, adds the real DIMENSION 11 echoes *#2*#5*#11#001#1*50## / *#2*0*#11#001#1*50## to both tests with their OWNd types pinned (OWNAutomationCommand dimension 11 vs OWNAutomationEvent dimension 10), and adds *2*7*0## so the "WHAT outside the table" case is pinned by a real frame. The guard itself is unchanged.
  4. This description: evidence hierarchy applied (live capture > reviewer's QEMU oracle replay, quoted as such > Encyclopedia > OWNd parser), state diagram updated, base rebased to 089fef5.

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 are in the issue_445 captures; the WHO 1 scope dim steps are in the 2026-10-25 F461 capture (on file) and the reviewer's oracle replays; the area 10 stop and the WHO 2 scope dimension frames rest on the reviewer's oracle replays, quoted as such.
  • 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 thread custom_components/myhome/gateway_events.py
Comment thread custom_components/myhome/gateway_events.py
Comment thread tests/test_gateway.py
Comment thread docs/configuration/lights.md Outdated
Comment thread .github/release-notes/2.0.0b16.md Outdated
@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
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.

@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.

Thanks for addressing the five logic and test findings cleanly (cdd2f4b7..5c2ccc0f). Before merging, we need to tighten the evidence and protocol framing in the PR description, docs, and test comments—relying on "OWNd parser truth" where no physical capture exists is not enough to claim real bus behaviour in our evidence hierarchy (live capture > QEMU firmware oracle > Encyclopedia / official spec > OWNd parser).

Auditing the cited fixtures and replaying PR #797's exact frames through the QEMU firmware oracle (MH200N 010108, F461 020011, and MyHomeServer1 028206) turned up three issues (including one from my own earlier note on issue_645/):

1. *2*0*100## in tests/fixtures/traces/issue_645/ is not a physical Area 10 stop capture

  • On #645 (#issuecomment-6111193057), @Interstellar0verdrive posted a prose timing summary with redacted <WHERE> placeholders ("*#2*<WHERE>## to ten different covers from one script"), without a raw JSON attachment.
  • myhome_trace_MyHomeServer1_cover_burst_latency_2026-10-11.json reconstructed those 10 redacted point-to-point covers as 91, 92, …, 99, 100 (91 + 9), accidentally turning the 10th point-to-point cover (0910 / 1001) into 100 (which in WHO 2 is Area 10), and wrote tx: *#2*100## → rx: *2*0*100## as a status reply rather than a stop command.
  • How to back *2*0*100## properly: Replaying down *2*0*100## against MyHomeServer1 028206 in own-firmware-oracle produces bus:$04B30A120A\r (Area 0x0A = 10, opcode 12 0A = Stop) and own:*2*0*100## on the event session (whereas down *#2*100## emits $04B30A1500\r, to which real actuators reply on their individual APL addresses, never *2*0*100##). Please drop the issue_645/ citation for Area 10 stop in ## Where are the traces and tests/test_gateway.py and cite the firmware oracle instead.

2. WHO 1 scope dim steps (*1*30*#5##, *1*31*3##, *1*30*0##, *1*30#2#100*#5##) are provable via the Firmware Oracle (and live gateway echo)—not "parser truth only"

  • ## What does the Firmware Oracle / Encyclopedia say currently states: "the oracle records what a gateway does with a request; this PR changes what MyHOME fires when such a frame is heard." In fact, own-firmware-oracle tests both down (OpenWebNet → SCS bus + event session) and up (inbound wall-switch SCS telegram → OpenWebNet event session).
  • Replaying PR #797's WHO 1 scope dim-step frames against real bt_luci binaries in QEMU confirms both directions empirically:
Direction & Stimulus MH200N 010108 F461 020011 / MyHomeServer1 028206
down *1*30*#5## ack, bus:$04B5051203\r, own:*1*30*#5## bus:$004B5051203\r / $04B5051203\r
down *1*31*3## ack, bus:$04B3031204\r, own:*1*31*3## bus:$004B3031204\r / $04B3031204\r
down *1*30*0## ack, bus:$04B1001203\r, own:*1*30*0## bus:$004B1001203\r / $04B1001203\r
down *1*30#2#100*#5## ack, bus:$06D50501320D030264\r, own:*1*30#2#100*#5## bus:$006D50501420D030264\r / $06D50501420D030264\r
up $17B5051203\r (wall keypad → #5) — MyHomeServer1: own:*1*30*#5##
up $17B3031204\r (wall keypad → 3) — MyHomeServer1: own:*1*31*3##
up $17B1001203\r (wall keypad → 0) — MyHomeServer1: own:*1*30*0##
up $18D50501320D030264\r (ext step → #5) — MyHomeServer1: own:*1*30#2#100*#5##
  • Even on the live F461 bench without a group dimmer, myhome.send_message *1*30*0## on a populated bus gets PIC-ACKed ($00800\r) by existing relays and echoed on the event session. Please either add/link a small suite in own-firmware-oracle (or cite these oracle verdicts + a live *1*30*0## check) rather than labelling WHO 1 dim steps "parser-truth only".

3. WHO 2 DIMENSION 10 on scope addresses (*#2*#5*10*…##, *#2*0*10*…##) is not normal actuator behaviour—frame it as a defensive parser guard

  • Physical shutters never reply with a collective WHERE (#5, 3, 0) on DIMENSION 10: a group or general scope has no single physical position (50%). Per ownkb:chunk:r000458 (functional/who-2-automation/dimensions.md) and functional/who-2-automation/addressing.md, scope requests/commands expand into one DIMENSION 10 frame per individual shutter (APL)—directly matching tests/fixtures/traces/issue_445/ (*2*10#001#1*0## followed by *#2*02*10*10*86*001*0##, *#2*03*10*10*76*001*0##, …, never *#2*0*10*…##).
  • What coso (F461 020011 / MyHomeServer1 028206) actually emits in QEMU:
    1. Scope DIMENSION 11 Go-To-Level writes (down *#2*#5*#11#001*50##, down *#2*0*#11#001*50##) echo on the event session as *#2*#5*#11#001#1*50## and *#2*0*#11#001#1*50##.
    2. coso's inbound 0x44 0x09 decoder is address-mode generic (up $018D2000144090A3200\r → own:*#2*0*10*10*50*010*0##, up $018D5050144090A3200\r → own:*#2*#5*10*10*50*010*0##—note 3-digit priority 010 / 001, not 0), so only a synthetic/third-party bus frame or OWNd parsing a grammatically valid scope DIMENSION 10 frame reaches that branch.
  • Requested updates:
    • Keep the is_not_dimension guard in gateway_events.py (it is a sound defensive invariant symmetrical with WHO 1), but update docs/configuration/covers.md, .github/release-notes/2.0.0b16.md, gateway_events.py, and the PR body / State Diagram so they do not present *#2*#5*10*10*50*…## / *#2*0*10*10*50*…## as routine physical shutter readbacks ("the shutter is stationary at 50 %" on #5 / 0).
    • Use coso's 3-digit priority (001 or 010) in the DIMENSION 10 test frames (*#2*#5*10*10*50*001*0##, *#2*0*10*10*50*001*0##) and include coso's real event-session DIMENSION 11 echoes (*#2*#5*#11#001#1*50##, *#2*0*#11#001#1*50##) in test_scope_events_fire_only_for_state_frames.

Inline comments below mark the specific places to adjust.

Comment thread custom_components/myhome/gateway_events.py Outdated
Comment thread docs/configuration/covers.md Outdated
Comment thread .github/release-notes/2.0.0b16.md Outdated
Comment thread tests/test_gateway.py Outdated
gdluck and others added 9 commits October 25, 2026 13:00
…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>
…enWebNet-HA#367)

The `issue_645/` burst trace reconstructed ten redacted point-to-point
status requests as WHERE 91..100, so its `*#2*100##` -> `*2*0*100##` pair
is a status reply on a reconstructed address, not a captured area 10 stop.
The scope-event test now cites the MyHomeServer1 028206 firmware oracle
replay of `down *2*0*100##` (`bus:$04B30A120A`, `own:*2*0*100##`) for that
frame instead; the frame and the pinned `myhome_area_automation_event`
`stop` are unchanged.

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

Live F461 2.0.11 capture of 2026-10-25 (plain OWNd master as sender, event
session recorded): `*1*30*0##` (general dim up, no dimmer on the plant) was
acknowledged in 69 ms, echoed on the event session and followed by every
point re-reporting its own state twice over about two seconds (`*1*0*06##`,
`*1*0*07##`, `*1*0*09##`, `*2*0*05##`); `*1*31*#1##` (group 1 dim down) was
acknowledged in 62 ms, echoed, and followed by `*1*0*06##` only.

The new test replays that sequence through `listening_loop` and pins that
no scope light or automation event fires, that every frame reaches the
point-to-point dispatch once in bus order, that both dim steps reach the
member re-sync, and that the leading point reports satisfy the group 1
sweep before it is armed. On the base branch it fails with two extra
events (`myhome_general_light_event` and `myhome_group_light_event` as
`on`). The capture is on file and will be ingested by a trace PR.

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

A physical shutter reports DIMENSION 10 on its own point-to-point address,
never on a group, area or general WHERE (issue_445 captures: one
`*#2*<APL>*10*...##` per actuator after a scope command). The
`is_not_dimension` guard on the WHO 2 scope events is therefore a defensive
parser invariant symmetrical with WHO 1, not the handling of a routine
readback: the dispatcher comment, the cover docs and the b16 release note
now say so, and no longer describe `*#2*michnovka#5*10*...##` as "the shutter is
stationary at 50 %".

The test frames use the 3-digit priority an F461 / MyHomeServer1 encodes
(`*#2*michnovka#5*10*10*50*001*0##`, `*#2*0*10*10*50*001*0##`), and the real
event-session echoes of a scope DIMENSION 11 level write
(`*#2*michnovka#5*michnovka#11#001#1*50##`, `*#2*0*michnovka#11#001#1*50##`) join
`test_scope_events_fire_only_for_state_frames` and the centralized stop
trigger test, so both OWNd dimension paths are pinned: plain OWNd master
parses the DIMENSION 11 echoes as `OWNAutomationCommand` (dimension 11,
never reaching the WHO 2 event branch) and the scope DIMENSION 10 frames as
`OWNAutomationEvent` (dimension 10, `is_opening` / `is_closing` False),
which the guard keeps away from `stop`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/events-state-frames branch from 99a12b3 to ef3417d Compare October 25, 2026 11:21
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks, all three are addressed on the branch (head ef3417d, rebased on 089fef5, one commit per point); the guard itself is unchanged.

  1. *2*0*100## and issue_645/: you are right, that fixture is a reconstruction (the burst trace rebuilt the ten redacted *#2*<WHERE>## as 91..100 and holds *#2*100## → *2*0*100## as a status reply). 9ebde27 drops the citation from tests/test_gateway.py; the body now says what the fixture really holds and cites your MyHomeServer1 028206 replay (down *2*0*100## → bus:$04B30A120A, own:*2*0*100##) as your QEMU verdict, since results/ at 52d354b has no row for it.
  2. WHO 1 dim steps: the oracle section is rewritten (the oracle tests both directions; your down / up table is quoted as your replay, no published row exists yet), every "parser truth only" wording is gone, and the live check you asked for is in. On the F461 2.0.11 plant today, *1*30*0## was ACKed in 69 ms, echoed on the event session and followed by every point re-reporting its state (*1*0*06##, *1*0*07##, *1*0*09##, *2*0*05##, twice over about 2 s); *1*31*#1## ACKed in 62 ms, echoed, followed by *1*0*06##. 9d6547e replays that sequence (test_general_dim_step_point_reports_fire_no_scope_events): no scope event, every frame a point event once, both steps reach the re-sync; on the base branch it fires two extra on events. Capture on file, fixture to follow in a trace PR.
  3. WHO 2 scope DIMENSION 10: reframed everywhere (dispatcher comment, covers.md, b16 release note, body and state diagram) as a defensive parser guard symmetrical with WHO 1, no "stationary at 50 %" on a scope address. The test frames use coso's 3-digit priority (*#2*#5*10*10*50*001*0##, *#2*0*10*10*50*001*0##) in test_gateway.py and test_device_trigger.py; your DIMENSION 11 echoes *#2*#5*#11#001#1*50## / *#2*0*#11#001#1*50## are in both tests with their OWNd types pinned (OWNAutomationCommand dimension 11, which never reached the branch, versus OWNAutomationEvent dimension 10 held back by the guard), and *2*7*0## pins the unknown-WHAT case (ef3417d). An independent second-opinion review of the diff pointed out that the release note implied the DIMENSION 11 echoes used to fire a stop; reworded.

4569 passed / 1 skipped (base 4559), 100 % coverage, ruff, mypy --strict, HA standards, docs sync and translations clean.

@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.

LGTM — all behavioural, protocol, evidence-hierarchy, and documentation points from both review rounds are cleanly addressed on ef3417de.

Verified against OWNd==2.0.0b11 merged onto v2-phase1-architecture (5641f917): 4,691 passed, 100.0% statement coverage across all touched modules, mypy --strict, ruff, verify_ha_standards.py, and sync_documentation.py --check clean. Thanks for the live F461 dim-step verification as well!

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit d465db8 into OpenWebNet-HA:v2-phase1-architecture Oct 25, 2026
17 checks passed
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