Skip to content

fix(validate): canonicalise 4-digit area-0 point addresses (0004 → 04) (#147) - #798

Merged
GreenGrassBlueOcean merged 10 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/validate-canonical-area0
Oct 25, 2026
Merged

GreenGrassBlueOcean merged 10 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/validate-canonical-area0

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

where: "0004" in myhome.yaml passes PointToPoint as typed, so the light (or switch, cover, member) gets the unique id …-1-0004 and sends *1*1*0004##. That spelling of an area-0 point is refused by 11 of 12 emulated models and by the live F461 2.0.11 and MyHomeServer1 3.87.13; the MH202 accepts it. Every gateway reports the point as 04, so the configured entity never sees its own status, its commands are NACKed, and bus discovery creates a second entity under …-1-04. WHO 2 follows the same WHERE grammar, so covers get the same treatment. Reported in #147. The 0809 vs 89 case of #135 (its comment 2496228553) is a point of area 8, not of area 0: this PR leaves it as typed (PointToPoint()("0809") == "0809") pending a fleet-wide 0101..0909 sweep, so #135 is not resolved here and stays open.

Refs #147, #713 (2-digit area-0 addresses 01-09); #135 for context only. No OWNd change is required: OWNd keeps 0004 as typed (OpenWebNet-HA/OWNd#123) and MyHOME canonicalises the yaml / registry spelling on its own; OpenWebNet-HA/OWNd#141 is withdrawn accordingly.

Based on the current v2-phase1-architecture (089fef5).

What is the solution

  • validate.py: new canonical_point_where(). A 4-digit point with A=0 and PL 1..9 (0004) is rewritten to the 2-digit form (04) with one WARNING per address per process, naming the spelling to write in myhome.yaml. Every other 4-digit input (0015, 0109, 0209, 0809, 1015, 0010, 0100, 0000) stays as typed, as OWNd does (feat(message): protocol boundary cleanup APIs for MyHOME #736 OWNd#123). PointToPoint applies it, so the light / switch / cover WHERE, the cover and light members and the impulse-cover check all resolve to the canonical form; the alarm schema passes canonical=False (WHO 5 addressing is an independent grammar, see the Encyclopedia records below). where_grammar.py holds the pure helpers canonical_point_spelling() / canonical_point_address() (the #4#II suffix kept) for every stored spelling; normalize_expose_where() applies them for WHO 1 / WHO 2, so the yaml expose: list and the UI expose flow (config_flow_expose.py, shared expose_entry_schema) store 04 for 0004 and the virtual actuator answers on the address the wall switch uses; binary_sensor_schema / sensor_schema canonicalise where when who == "1" (motion / illuminance) on the whole mapping, since SpecialWhere() cannot see the WHO.
  • migrate.py: migrate_canonical_point_addresses() runs at the end of migrate_entry_and_registries, before the platforms load, wrapped so a registry error is a WARNING and never blocks the setup. Entity unique ids {mac}-{1|2}-0004[#4#if][-suffix] (light, switch, cover, button) move to …-04…; device identifiers are rewritten the same way. Hardened in this revision:
    • the duplicate lookup is scoped to this config entry: a …-04 entity held by another config entry is a collision, nothing is touched and a WARNING names both entities;
    • the registries refuse a rename to an id in use, so this entry's bus-created twin is taken out first and the rename done next; if the rename is refused the twin is put back as it was (the entity registry restores entity_id, name, icon, area and labels; the device registry restores id, area, labels and user-given name), so nothing is lost on failure;
    • the twin device's remaining entities, area_id and name_by_user are carried over to the surviving device (only where the surviving device has none);
    • running the migration again finds nothing to do (pinned by the test).
    • the binary_sensor and sensor domains are covered; the WHO in the unique id is the predicate ({mac}-1-0004-motion moves, {mac}-25-0004, {mac}-4-0004-temperature, {mac}-18-0004-power and the WHO-less legacy {mac}-0004-illuminance do not match);
    • expose ConfigSubentries holding 0001..0009 (WHO 1 / WHO 2, #4#II kept) are rewritten through the subentry API (data and title); when the entry also has the canonical binding, that one stays and the 4-digit twin is removed with a WARNING;
    • entry.options["cover_travel_times"] is re-keyed (0004 / 0004#4#01 -> 04 / 04#4#01); a record already under the canonical key wins; _stored_calibration() also falls back to the 4-digit key;
    • the light that bus discovery created at the address of a WHO 1 switch or impulse cover declared as 0004 (unique ids are scoped per domain, so the same-domain lookup never saw it) is removed together with its -disable / -enable / -calibrate buttons, instead of being handed to the merged device and later removed by light.py with the buttons left orphaned; the check also runs for an owner that is already canonical, so a retry is idempotent;
    • the twin device's labels are carried (union) and the removed duplicate entity's area_id, icon, name and labels are carried onto the surviving entity where it has none (what the survivor has wins);
    • WHO 1 motion / illuminance bus twins (review 5642942172, item 1): bus discovery spells these sensors without the WHO ({mac}-04-motion, {mac}-04-illuminance: binary_sensor.py build_motion, sensor.py build_illuminance; only the device identifier carries {mac}-1-04), so the same-domain lookup under {mac}-1-04-motion never found them and the platform's restore decided by registry order which of the two entries for 04 survived. For a binary_sensor / sensor entity whose id is {mac}-1-0[1-9]…-<suffix> the migration now also looks up this entry's {mac}-0[1-9]…-<suffix> twin, after the rename and when the yaml entity is already canonical (a later boot, or a yaml that said 04 from the start), carries its unset area_id / icon / name / labels and removes it; a twin of another config entry is left alone;
    • the registry spelling is rewritten silently: the one-shot "Write '04' in myhome.yaml" WARNING stays in validate.py, where the user can act on it.
  • legacy_yaml.py: canonicalise_imported_platforms() moves the copy of myhome.yaml stored in entry.options["migrated_yaml_platforms"] (arch(discovery): gate poll-on-add across all paced sweeps, probe ON dimmers, and migrate myhome.yaml once into ConfigEntry #733) to the canonical spelling: where, members and the device key (1-0004 -> 1-04, the #4#II suffix kept), in a new copy stored through the options API; a copy already holding the canonical key keeps that device and drops the 4-digit twin with a WARNING. It runs on the read path (_apply_imported_copy(), before the registry migration and the platforms) and as a guard on the write path. Without it the imported copy bypassed the validator: boot 1 renamed the registry entry, restored it without its configuration and re-created 0004 as light.<name>_2, and every later boot churned _2 -> _3 -> ... (review item 1).
  • Release note: the carried fields, the ghost-light cleanup and that the A / PL state attributes of a light, switch or cover declared as 0004 now read 0 / 4 instead of 00 / 04, as on bus-discovered entities.
  • tests/test_validate.py: the "0101" case keeps its value (only area 0 is rewritten) with a comment saying why.
  • Docs (lights.md) and the release-notes delta (which now names the removed duplicate).

Companion PR: none: no OWNd change is required (OWNd keeps 4-digit input as typed, OpenWebNet-HA/OWNd#123; OpenWebNet-HA/OWNd#141 is withdrawn for that reason).

State Diagram

stateDiagram-v2
    direction TB

    state "1. yaml WHERE through PointToPoint" as Yaml {
        [*] --> Typed : where 0004 in myhome.yaml
        Typed --> Canonical : A=0, PL 1..9\n04 + one WARNING\nunique id ...-1-04, sends *1*1*04##
        Typed --> AsTyped : 0015 / 0109 / 0209 / 0809 / 1015 / 0010 / 0100 / 0000\nkept as typed , OpenWebNet-HA/OWNd#123
    }

    state "2. registry migration at setup (this config entry)" as Registry {
        [*] --> Old : entity ...-1-0004 [+ bus twin ...-1-04]
        Old --> Collision : twin belongs to another config entry\nWARNING, nothing touched
        Old --> TwinOut : twin of this entry removed\n, entities, area, name carried
        TwinOut --> Renamed : rename ...-1-0004 to ...-1-04 succeeds\ntwin stays removed
        TwinOut --> Restored : rename refused , ValueError\ntwin re-created as it was, WARNING
        Renamed --> SensorTwinOut : WHO 1 motion / illuminance\nbus twin ...-04-motion removed , area, icon, name, labels carried
        Renamed --> Renamed : second run - nothing to do
    }
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 MyHOME #799 (merged).

Live retest (2026-10-24)

Combined-build re-test, not an ingested fixture (the raw recordings are not committed; it was listed under "Where are the traces" before and is summarised here instead, review 5642942172 item 3). Consensus label: confirmed for the tested case (combined build, F461 2.0.11, HA 2026.10.2). A yaml variant with light_2: where: "0007" logged one WARNING … WHERE '0007' is the 4-digit spelling of the point A=0 PL=7; using '07' … at startup; light.light_2 kept its unique id …-1-07 (no duplicate entity), tx *1*1*07## / rx *1*1*07## and tx *1*0*07## / rx *1*0*07## were written and followed; the yaml was restored afterwards. The registry merge of a pre-existing …-1-0004 entity was not exercised live (the plant never had one); it rests on the unit tests above.

How is it tested

  • tests/test_canonical_point_where.py (61): the validator table (14 cases: 1 rewrite, 13 kept as typed, including 0209, 0809, 0101, 0109, 0015, 1015, 0010, 0100, 0000); one WARNING per rewritten address; the silent helpers' table (12 cases: 0000, 0010..0015, 1015, #4, 04#4#01 kept, 0004 / 0009#4#01 rewritten, no log record); yaml device keys use the canonical spelling (1-04, 2-05); a light configured as 0004 is set up once with unique id …-1-04 and its is_on follows *1*1*04## through the dispatcher without a duplicate being discovered; the registry migration moves …-1-0004 / …-1-0004-disable to …-1-04…, removes the bus duplicate entity and device, hands the bus device's other entity, area, labels and user-given name over, carries the duplicate entity's area, icon, name and labels, keeps …-1-0015 and light.kitchen, and a second run changes nothing; the survivor's own customisations win; a …-2-0009#4#01 cover and its device are renamed without a WARNING; registry ValueErrors are logged and leave the ids untouched; a refused rename puts the bus twin back; a …-04 entity of another config entry is a collision.
    • Imported copy (review item 1): the rewrite table (light, button alias, routed cover, members; WHO 25 / WHO 5 / 0015 untouched; the stored dict not mutated; idempotent), the collision case, the write-path guard, and the two-boot scenario: a snapshot with 0004, no myhome.yaml, real entity and device registries and the light platform; both boots give one …-1-04 light that keeps light.kitchen, its device and dimmable, no _2, and the second boot migrates nothing.
    • Expose (item 2): the grammar table (9 cases, WHO 4 / WHO 18 untouched, no log record), the shared expose_entry_schema and the resulting binding key, the subentry migration (two moved, 0015, a WHO 4 probe, the clock and a non-expose subentry untouched, the 0007 / 07 collision resolved for 07 with a WARNING, second run silent).
    • WHO 1 sensors (item 3): both schemas (0004 -> 04, warnings), WHO 25 / 9 / 4 / 18 and 0015 untouched; the registry case now seeds the real bus unique ids {mac}-04-motion / {mac}-04-illuminance (the light tests' hanging bus sensor too): both twins removed, their area / icon / name / labels carried where the yaml entity has none (the illuminance survivor keeps its own icon and labels), a {mac}-05-motion of another config entry untouched, the twin device merged, four other-WHO entries untouched, and the second run pinned as no log line and no registry write (every EntityRegistry / DeviceRegistry write patched to raise); the already-canonical case ({mac}-1-04-motion next to {mac}-04-motion, routed 09#4#01 too; 1-15-motion, 0015, WHO 4 04-temperature not looked up); the vanished-survivor guard.
    • End to end through the platforms: binary_sensor.async_setup_entry with {mac}-04-motion listed before {mac}-1-0004-motion in the registry (real registries, where: "0004" in the configured platform): one MyHOMEMotionSensor with unique id {mac}-1-04-motion, binary_sensor.hall_motion kept with the twin's customisations, no "removed stale registry entry", and *1*34*04## reaches it without a second entity being discovered; the same with an already canonical {mac}-1-04-motion sharing the device {mac}-1-04 with the twin listed first (no rename, no device hand-over to reorder the registry: this is the order that removed the user's configured entity before the fix); sensor.async_setup_entry with {mac}-04-illuminance listed first (before the fix the restore pruned both entries at the configured address and re-created the yaml one bare: the twin's customisations were lost).
    • Cover calibration (item 4): the re-keying (plain and routed keys, 0015 and 21 untouched, the 0005 / 05 collision resolved for 05, second run writes nothing) and the read fallback.
    • Ghost light (item 5): a WHO 1 impulse cover …-1-0004 with a bus light …-1-04 and its two buttons: after the migration the cover is …-1-04 on its device, the light and both buttons are gone, another entry's light at the canonical id is untouched; reproduced before the fix (the light and both buttons sat on the cover's device); the already-canonical owner case and a …-1-15 light next to a switch (not an area-0 point, untouched).
  • Fail-before, per commit, on the preceding commit: silent helper 2 (import) + the WARNING assertion; imported copy 4; expose 4; WHO 1 sensors 2 of 3; cover calibration 2; ghost light and carried fields 2 of 3; bus sensor twins 5 of the 5 new / rewritten tests (the twin-first motion case fails with {mac}-04-motion restored and binary_sensor.hall_motion gone; the other four with the customisations not carried). On v2-phase1-architecture (d8bac15) the module does not import (canonical_point_where is new); with that import guarded, 9 of the original 22 fail.
  • Full suite against a plain upstream OWNd master checkout (f04d8aa) on PYTHONPATH, HA 2026.10.2, Python 3.14, -n 4: 4620 passed, 1 skipped, coverage 100 % (verify_ownd_coverage.py: 16,272 / 16,272 statements) (base 089fef5, same setup: 4559 passed, 1 skipped).
  • scripts/verify_ownd_coverage.py: 100 % statement coverage. ruff check . (ruff 0.17.2 and 0.16.7, which reported the I001 of item 2), scripts/typing_ratchet.py (mypy --strict, 0 errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/manage_translations.py check, tests/test_fixture_privacy.py pass. Fixture MAC 00:03:50:00:01:47, no hosts.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 1 area-0 point spellings trace PR: #799 (merged), tests/fixtures/traces/f461_live_2026-10-23/…json, run2_reviewer_reads and run1_oracle_replay_readonly; tests/test_f461_live_2026_10_23_trace.py::test_area0_point_spellings_0007_nack_07_ack_7_is_area_7 BTicino F461 2.0.11, BT-F411/4 at WHERE 07 (A=0 PL=7) *#1*0007## → NACK in 7 ms (refused by the gateway itself); *#1*07## → *1*0*07## in 88 ms; *#1*7## → area 7 (ACK, no data); *#1*0001##, *#1*0002## → NACK in 5-7 ms. The gateway reports the point as 07 and never as 0007.
WHO 1 four-digit points on a second gateway family #799, tests/fixtures/traces/myhomeserver1_live_2026-10-24/…json, run3_reads_lights_addresses_humidity and run2; tests/test_myhomeserver1_live_2026_10_24_trace.py::test_four_digit_area0_points_are_devices_distinct_from_the_two_digit_ones, ::test_general_light_status_lists_four_digit_and_two_digit_points Legrand / BTicino MyHomeServer1 firmware 3.87.13 (*#13**16*3*87*13##, WHO 1013 model 67), status reads only *#1*0001## NACK in 8 ms and *#1*0004## in 14 ms (refused at once) vs *#1*01## / *#1*04## NACK after 2.0 s (forwarded, no device there); *#1*0010## → *1*0*0010##, *#1*0014## → *1*0*0014## while *#1*14## → *1*0*14## (two different devices): 0010-0015 must stay as typed and only 000N is canonicalised, which is what the validator does. Note: the oracle emulates MyHomeServer1 2.82.06; this unit is newer.
WHO 1 area-0 commands in both spellings tests/fixtures/traces/f461_live_2026-10-15/myhome_trace_F461_live_2026-10-15.json, run probe_area0 (rows, command session) BTicino F461 2.0.11 *#1*0001## NACK in 13 ms, *1*1*0001## NACK in 11 ms, *1*0*0001## NACK in 16 ms, *#1*0004## NACK in 12 ms (all refused by the gateway itself, no bus event) vs *#1*01## ACK in 72 ms with the reply *1*0*01##, *1*1*01## ACK in 39 ms followed by *1*1*01## on the event session, *1*0*01## ACK in 56 ms followed by *1*0*01##; *#1*02## NACK after 2.0 s (forwarded, no device). The 4-digit spelling of an area-0 point is refused for commands as well as reads.
WHO 2 area-0 points reported in two digits next to PL 10..15 in four tests/fixtures/traces/issue_445/myhome_trace_MyHomeServer1_LN4660M2_centralized_down_then_stop_2026-09-26.json Legrand / BTicino MyHomeServer1 the same plant reports *2*0*02##, *2*0*03##, *2*0*04##, *2*0*08##, *2*0*09## and *#2*02*10*10*61*001*0##, *#2*03*10*10*50*001*0##, *#2*04*10*10*0*001*0##, *#2*08*…, *#2*09*… next to *2*0*0010##, *2*0*0011## and *#2*0010*10*10*0*001*0##, *#2*0011*…: A=0 PL 1..9 in two digits, PL 10..11 in four, as for WHO 1.
WHO 2, second family tests/fixtures/traces/issue_466/myhome_sweep_H4890_all_2026-09-26T11-48-10.json BTicino H4890 4.0.15 *2*0*05##, *2*0*06##, *2*0*08##, *2*1*05##, *2*1*06##, *2*1*08##, *2*1000#0*05##, *2*1000#1*05##, *#2*05*10*10*0*000*0##, *#2*06*10*11*0*000*0## (never 0005 / 0006 / 0008).
WHO 2, third family tests/fixtures/traces/issue_619/myhome_sweep_F453AV_all_2026-10-04T19-45-11.json BTicino F453AV 3.0.14 *2*0*05##, *2*0*06## in the *#2*0## sweep reply, next to *2*0*69##, *2*0*74## … (never 0005 / 0006).
WHO 2 point poll on the test plant tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, run1_oracle_replay_readonly step plant_reads (poll_cover_05), run4_final_state, run4b_final_state_after_restore BTicino F461 2.0.11, BT-F411/2 shutter at WHERE 05 *#2*05## ACK in 82 ms with the reply *2*0*05## (108 ms and 82 ms in the two later runs); the gateway reports the shutter as 05. No 4-digit WHO 2 poll or command was sent to this plant.

What does the Firmware Oracle / Encyclopedia say

Suite lights-area0 (results/<P>/<V>/oracle/full/lights-area0.tsv, published index at 53f9514), per frame:

Frame nack / silent (refused by the gateway itself) nack / out (forwarded to the bus, no device in the emulation) ack / out
*#1*0001##, *#1*0002##, *1*1*0001## F450, F453, F453AV, F454, F459, F460, F461, H4684, MH200, MH200N, MyHomeServer1 2.82.06 (11 of 12) – MH202 (*1*1*0001## → bus $0301001200, own *1*1000#1*01##)
*#1*02## F450, H4684, MH200 2.1.2 (the #684 case, not touched here) F453, F453AV, F454, F459, F460, F461, MH200N, MyHomeServer1 (bus $0302001500 / $00302001500) MH202

So the 2-digit form is the one the gateways put on the bus, and the 4-digit 000N form is refused everywhere except on the MH202. No oracle stimulus exists for the registry migration (it touches no frame).

lights-area0 is a WHO 1 suite only (its stimuli are *#1*…## / *1*…##: 0, 00, 0001, 0002, 0002#4#01, 0010, 0010#4#01, 0015, 02, 2 and the interface variants). 4-digit WHO 2 polls and commands (*#2*000N##, *2*1*000N##) have not been replayed in the oracle: no results/**/*.tsv at 52d354b holds such a stimulus. The WHO 2 side of this PR rests on the shared A / PL grammar (Encyclopedia records below) and on the four committed physical traces in "Where are the traces", which show four gateway families reporting WHO 2 area-0 points PL 1..9 in two digits next to PL 10..11 in four; a WHO 2 replay suite is a follow-up for own-firmware-oracle.

OpenWebNet Encyclopedia Machine records (ownkb):

  • ownkb:chunk:r000264, ownkb:chunk:r000450, ownkb:chunk:r000451, ownkb:chunk:r000848: the SCS A / PL point-to-point addressing family shared by WHO 1 (lighting) and WHO 2 (automation). The oracle rows above are WHO 1 stimuli; WHO 2 is covered by inference through the shared A / PL grammar, not by a separate WHO 2 oracle stimulus, which is why the cover and members WHERE take the same rewrite.
  • ownkb:chunk:r000604 / ownkb:claim:c003426: WHO 5 (burglar alarm) addressing is an independent grammar (zones, sensors, central unit), not the A / PL family, which is why alarm_control_panel_schema passes PointToPoint(canonical=False) and 0004 stays as typed there.

Review follow-up (review 5639727669)

Item Commit
P3 silent helper: no "Write '04' in myhome.yaml" WARNING during the registry migration (inline comment on migrate.py) 240310a refactor(where_grammar)
P1 imported myhome.yaml copy (migrated_yaml_platforms) canonicalised on read and write, churn scenario tested d0cae00 fix(legacy_yaml)
P2 expose (normalize_expose_where, _validate_expose_entry, config_flow_expose.py) and the expose subentries 24f47aa fix(expose)
P2 WHO 1 binary_sensor / sensor schemas and the binary_sensor / sensor domains in the migration 9384880 fix(validate)
P2 cover_travel_times re-keyed da60ca1 fix(migrate)
P3 ghost light + orphaned buttons of a WHO 1 impulse cover; labels on the device merge; area_id / labels / icon from the removed duplicate; release note on the A / PL attributes c88e12f fix(migrate)
Description: #799 (merged), base SHA d8bac15, solution item 1, Encyclopedia provenance this body

The first commit (fix(validate)) is unchanged.

Review follow-up (review 5642942172)

Rebased on v2-phase1-architecture 089fef5 (the seven PR commits unchanged in content; shas moved).

  1. [P1] WHO 1 motion / illuminance bus twins {mac}-04-<class> looked up, their unset customisations carried, removed; already-canonical entities covered on later boots; tests seed the real bus unique ids and run end to end through binary_sensor / sensor async_setup_entry with the twin listed first, including the order that removed the user's entity -> 5fb1f18 fix(migrate). Your reading of build_motion / MyHOMEMotionSensor and build_illuminance / MyHOMEIlluminanceSensor is confirmed. One precision on the two consequences: in the straightforward boot (yaml {mac}-1-0004-motion on {mac}-1-0004, twin on {mac}-1-04) the device hand-over re-inserts the twin after the renamed yaml entity, so the restore removed the twin and only its customisations were lost; the order that removes the user's entity is the one without a hand-over (both on {mac}-1-04, e.g. a later boot), which the new twin-first test reproduces. Either way the fix makes the registry order irrelevant.
  2. [P2] ruff 0.16.7 I001 on the inline import in _remove_ghost_light (ruff 0.17.2 did not flag it): the rationale comment moved above the import -> bd9de12 style(migrate); ruff check custom_components tests scripts passes with 0.16.7 and 0.17.2, scripts/verify_ha_standards.py passes.
  3. [P2] "Where are the traces": the un-ingested combined re-test row moved to Hardware Verification; the f461_live_2026-10-15 probe_area0 command rejections and the four WHO 2 traces (issue_445 MyHomeServer1 / LN4660M2, issue_466 H4890, issue_619 F453AV, f461_live_2026-10-23) cited with the frames as they are in the fixtures; the oracle section states that lights-area0 is WHO 1 only and that 4-digit WHO 2 polls / commands have not been replayed -> this body.
  4. [P3] #135 dropped from the release note, the PR title and the validate.py / migrate.py docstrings (the test module docstring too); the release note and the validator docstring say the 0809 of My home stopping Update #135 is a point of area 8 and stays as typed, so My home stopping Update #135 stays open; base SHA updated to 089fef5 -> f764154 docs(validate).

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): live F461 2.0.11 and MyHomeServer1 3.87.13 captures in 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, the committed f461_live_2026-10-15, issue_445, issue_466, issue_619 and f461_live_2026-10-23 traces, and the published lights-area0 oracle rows above.
  • V2 Architecture: no entity added; the registry migration uses the HA entity and device registries.
  • 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!

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
…penWebNet-HA#136-OpenWebNet-HA#142 and MyHOME OpenWebNet-HA#791-OpenWebNet-HA#798

The maintainer asked on OpenWebNet-HA/OWNd#137 (comment 6384484511) for the
physical trace behind the 2026-10-24 pull requests to be added to this repo
like every capture before. Same plant and format as f461_live_2026-10-15 and
f461_live_2026-10-18: a BTicino F461 2.0.11 (WHO 13 device type 200, WHO 1013
134.15.5.0) with BT-F411/4 lights at 06 / 07, an F422 light at 09 whose
actuator lacked configurator 2 at capture time, a BT-F411/2 5 s shutter at 05
and a KW4691 standalone thermostat at zone 2 with a BT-F430/2 valve and a
BT-F430/4 fan coil; no central unit.

tests/fixtures/traces/f461_live_2026-10-23/: the derived trace archive (299
event-session frames, 939 tx / rx / result rows in ten runs: the oracle replay
of 165 read-only requests, reviewer reads, setpoint restore, final state,
report-form writes, the event session during a local Home Assistant run, the
bus monitor of an earlier run, two flicker re-tries) and its README with the
plant, the runs, what the plant established and which pull request each frame
backs. MAC / IP are synthetic in both their text and WHO 13 byte forms.

tests/test_f461_live_2026_10_23_trace.py pins the facts the pull requests rely
on: the *michnovka#4*2## reply order with dimension 12 before and different from 14,
*michnovka#4*2*20## followed by two actuator frames and *michnovka#4*#0*20## answering both,
*#1*0007## NACK vs *#1*07## ACK vs *#1*7## empty, *OpenWebNet-HA#13**0## echoed on the
event session, every WHO 16 request refused in under 10 ms, the dimension 27
model name frame, *michnovka#4*#2*14## and the other central-unit forms refused, one
*2*0*0##, the shutter's own stop 5.1-5.9 s after each run; plus entity replays
through handle_event that pass on the base branch with the pinned OWNd.

tests/test_trace_oracle_validation.py: tier 4 tolerates the authentic
*OpenWebNet-HA#13**27*70#52#54#49## reply until the pinned OWNd carries the dimension 27
decoder (OWNd#139); it parses to None without raising. The *4*311*2## NACK /
*4*311*#2## ACK pair cited by OWNd#137 is already in f461_live_2026-10-15 and
is referenced, not duplicated. scripts/update_trace_matrix.py leaves the
matrix unchanged (the F461 row already covers WHO 1, 2, 4, 13, 1013).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit that referenced this pull request Oct 24, 2026
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
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
MyHomeServer1 README: "OWnd" typo; the alarm gate, the config-flow
identification and the liveness probe rows name MyHOME OpenWebNet-HA#803, OpenWebNet-HA#804, OpenWebNet-HA#806.
F461 README: the "What this capture backs" table extends to MyHOME OpenWebNet-HA#800-OpenWebNet-HA#806
(OpenWebNet-HA#802 the dimension 27 wire form, OpenWebNet-HA#803 *michnovka#5*0##, OpenWebNet-HA#804 the WHO 13 / 1013
identification replies, OpenWebNet-HA#806 the *OpenWebNet-HA#13**0## echo; OpenWebNet-HA#800 / OpenWebNet-HA#801 / OpenWebNet-HA#805 have no
frame in this capture); it states that f461_live_2026-10-15 holds the
dimension 22 reboot behind OWNd OpenWebNet-HA#136, the *4*311*2## / *4*311*#2## pair
behind OWNd OpenWebNet-HA#137 and probe_area0 behind OWNd OpenWebNet-HA#141 / MyHOME OpenWebNet-HA#798; and one
sentence says reply_time_s is measured while tx.t is reconstructed from the
probe log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/validate-canonical-area0 branch from 5033698 to 2336301 Compare October 24, 2026 15:48
GreenGrassBlueOcean pushed a commit that referenced this pull request Oct 24, 2026
MyHomeServer1 README: "OWnd" typo; the alarm gate, the config-flow
identification and the liveness probe rows name MyHOME #803, #804, #806.
F461 README: the "What this capture backs" table extends to MyHOME #800-#806
(#802 the dimension 27 wire form, #803 *#5*0##, #804 the WHO 13 / 1013
identification replies, #806 the *#13**0## echo; #800 / #801 / #805 have no
frame in this capture); it states that f461_live_2026-10-15 holds the
dimension 22 reboot behind OWNd #136, the *4*311*2## / *4*311*#2## pair
behind OWNd #137 and probe_area0 behind OWNd #141 / MyHOME #798; and one
sentence says reply_time_s is measured while tx.t is reconstructed from the
probe log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the fix/validate-canonical-area0 branch 2 times, most recently from f673b6c to 71bed53 Compare October 24, 2026 19:36

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

Keeping OWNd faithful to the paper specification (OpenWebNet-HA/OWNd#123) while canonicalising 0001..0009 → 01..09 at the MyHOME boundary is the right architectural split, and the core PointToPoint + twin-merge + rollback design in migrate.py is clean (verified 141/141 tests passing against both dev OWNd and pinned OWNd==2.0.0b11, plus ruff, verify_ha_standards.py, and sync_documentation.py --check, and confirmed HA _deleted_devices restores id, area_id, name_by_user, and labels on refused device renames).

However, because OpenWebNet-HA/OWNd#141 was withdrawn (OWNd.normalize_where("0004") == "0004" and is_apl_address("0004") is True), several paths that bypass PointToPoint still keep "0004", including one P1 infinite entity-churn regression and three P2 gaps, plus a few stale/inaccurate claims in the PR description (#issue-6013207487).


Summary of Requested Changes

  1. P1 — Imported myhome.yaml snapshot (CONF_MIGRATED_YAML_PLATFORMS) causes infinite entity churn & drops YAML config (custom_components/myhome/legacy_yaml.py, migrate.py):

    • In 2.0 betas after #733, migrate_legacy_yaml_to_entry_options() stored entry.options["migrated_yaml_platforms"] with "where": "0004" and raised legacy_yaml_imported telling users myhome.yaml can be deleted.
    • When myhome.yaml is gone, _apply_imported_copy() loads that stored snapshot directly via _apply_yaml_platforms() without passing through validate.config_schema().
    • On boot: (a) migrate_canonical_point_addresses() renames {mac}-1-0004 → {mac}-1-04; (b) PlatformDiscovery.restore() restores {mac}-1-04 with cfg={} because config_for() misses "04" in configured_platforms (losing dimmable, lock_features, rgb, color_temp, model), recording only "04" in self.known; (c) PlatformDiscovery.configure() sees "0004" not in self.known and re-creates {mac}-1-0004 (light.<name>_2). On every subsequent restart, migrate_canonical_point_addresses() deletes {mac}-1-04 as a "duplicate" and renames {mac}-1-0004 → {mac}-1-04, churning light.<name>_2 → _3 → _4 on every boot.
    • Action: Canonicalise point where values (and re-key aliases) in entry.options[CONF_MIGRATED_YAML_PLATFORMS] during migration and/or inside _apply_imported_copy().
  2. P2 — expose (myhome.yaml and UI config_flow_expose.py) accepts "0004" un-canonicalised:

    • Contrary to item 1 of ## What is the solution, where_grammar.normalize_expose_where() delegates WHO 1/2 to OWNd.normalize_where(raw) ("0004") and _validate_expose_entry() (shared by YAML expose: and config_flow_expose.py) accepts "0004" because is_apl_address("0004") is True.
    • At runtime, an exposed virtual WHO 1/2 actuator at "0004" transmits *1*...*0004## (NACKed/dropped by 11/12 gateways), never matches incoming wall-switch commands or status reads on "04", and bypasses duplicate-binding / is_exposed_on_bus(..., "04") checks.
    • Action: Canonicalise 0001..0009 → 01..09 for WHO 1/2 in normalize_expose_where() (preserving optional #4#II interface suffix) and migrate any existing expose ConfigSubentries holding 0001..0009.
  3. P2 — WHO 1 binary_sensor (motion) and sensor (illuminance) are neither canonicalised nor migrated:

    • binary_sensor_schema (who: "1") and sensor_schema (who: "1", class: illuminance) validate where via SpecialWhere() rather than canonicalising WHO 1 4-digit area-0 points, and _POINT_ADDRESS_DOMAINS in migrate.py omits "binary_sensor" and "sensor".
    • A WHO 1 sensor configured at where: "0004" stays "0004" (key "1-0004"), polls *#1*0004*5/6/7## (NACKed on 11/12 gateways), misses "04" frames in _spellings(), and lets bus discovery create a duplicate {mac}-1-04-motion / {mac}-1-04-illuminance.
    • Action: Canonicalise where when who == "1" in binary_sensor_schema / sensor_schema and include "binary_sensor" and "sensor" in migrate_canonical_point_addresses().
  4. P2 — Persisted cover calibration / slat_time (CONF_COVER_TRAVEL_TIMES) is orphaned on migration:

    • entry.options["cover_travel_times"] (cover_calibration._stored_calibration() / MyHOMECover._persist_calibration()) is keyed by device_id ("0004" or "0004#4#01"). After migrating a cover to "04" / "04#4#01", _stored_calibration(entry, "04") returns None, silently losing measured travel times or slat_time overrides.
    • Action: Re-key 0001..0009 (with optional #4#II) in entry.options[CONF_COVER_TRAVEL_TIMES] during migration.
  5. P3 — Minor migration & logging cleanups (see inline comments):

    • Split a silent helper from canonical_point_where() so migrate.py does not log "Write '04' in myhome.yaml to silence this warning" when myhome.yaml is already updated or absent.
    • Handle cross-domain WHO 1 ghost light + orphaned -disable/-enable companion buttons when a WHO 1 impulse cover (who: "1", where: "0004") is migrated.
    • Carry labels on device merge (and consider carrying area_id/labels/icon from a removed duplicate entity when the surviving YAML entity has none), and note in the 2.0.0b16 release note if relevant that light/switch state attributes A/PL change from "00"/"04" to "0"/"4" (matching bus-discovered entities).

PR Description (#issue-6013207487) Updates Needed

  1. Contradictory #799 status: ## Hardware Verification says captures in MyHOME #799 (merged), whereas row 1 of ## Where are the traces still says trace PR: #799 (open) (#799 merged as ad8431ff).
  2. Stale base SHA (ddc6d601): Both ## What is the problem and ## How is it tested still cite v2-phase1-architecture (ddc6d601) after the rebase onto d8bac157.
  3. config_flow_expose.py claim: Update item 1 of ## What is the solution once WHO 1/2 expose canonicalisation is wired in.
  4. Encyclopedia provenance in ## What does the Firmware Oracle / Encyclopedia say: Add the OpenWebNet Encyclopedia Machine KB records alongside the lights-area0 oracle table — specifically ownkb:chunk:r000264, r000450, r000451, r000848 (shared SCS A/PL family for WHO 1 & WHO 2, noting WHO 2 is inferred via shared A/PL grammar rather than a separate WHO 2 oracle stimulus) and ownkb:chunk:r000604 / ownkb:claim:c003426 (WHO 5 alarm addressing is an independent grammar, justifying PointToPoint(canonical=False) on alarm_control_panel_schema).

pass

try:
migrate_canonical_point_addresses(hass, entry)

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 config-entry options stores also hold 4-digit area-0 point spellings and need migration alongside the entity/device registries (plus expose subentries):

  1. P1 — entry.options["migrated_yaml_platforms"] (CONF_MIGRATED_YAML_PLATFORMS in legacy_yaml.py): If a user's myhome.yaml was imported by #733 in an earlier 2.0 beta and myhome.yaml was then deleted, _apply_imported_copy() loads the stored snapshot without validate.config_schema(). After migrate_canonical_point_addresses() renames {mac}-1-0004 → {mac}-1-04, PlatformDiscovery.restore() misses config_for(..., "04") (dropping dimmable/lock_features/rgb/model) and PlatformDiscovery.configure() immediately re-creates {mac}-1-0004 (light.<name>_2), causing an entity-deletion/re-creation churn loop on every restart.
  2. P2 — entry.options["cover_travel_times"] (CONF_COVER_TRAVEL_TIMES in cover_calibration.py): Keyed by device_id ("0004" / "0004#4#01"). Once the cover moves to "04" / "04#4#01", _stored_calibration(entry, "04") returns None, orphaning calibrated travel times and slat_time overrides.

Comment thread custom_components/myhome/migrate.py Outdated


# Platforms whose unique ids carry a WHO 1 / WHO 2 point address: {mac}-{who}-{where}[#4#{iface}][-{suffix}].
_POINT_ADDRESS_DOMAINS = ("light", "switch", "cover", "button")

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.

_POINT_ADDRESS_DOMAINS omits "binary_sensor" and "sensor". When who: "1" is configured on binary_sensor (motion) or sensor (illuminance) with where: "0004", their unique IDs are {mac}-1-0004-motion and {mac}-1-0004-illuminance (which _POINT_ADDRESS_ID already matches via (?P<tail>(?:[#-].*)?)), and bus discovery creates {mac}-1-04-motion / {mac}-1-04-illuminance duplicates. Adding "binary_sensor" and "sensor" here (paired with canonicalising WHO 1 where in binary_sensor_schema / sensor_schema) covers them cleanly.

Comment thread custom_components/myhome/migrate.py Outdated
"""``1-0004`` -> ``1-04`` (also ``2-0009#4#01``, ``1-0004-disable``); None when nothing is rewritten."""
if (match := _POINT_ADDRESS_ID.match(after_mac)) is None:
return None
canonical = canonical_point_where(match["where"])

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.

Calling canonical_point_where() here emits the YAML warning ("Write '04' in myhome.yaml to silence this warning") during registry migration even when the user has already updated myhome.yaml to '04' (or never had a myhome.yaml). Extracting a pure/silent rewrite helper for migrate.py and keeping the one-shot LOGGER.warning(...) in validate.py avoids false warnings on startup.

if (new_tail := _canonical_point_id(reg_entry.unique_id[len(prefix):])) is None:
continue
new_unique_id = f"{prefix}{new_tail}"
duplicate_id = entity_registry.async_get_entity_id(reg_entry.domain, DOMAIN, new_unique_id)

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.

Cross-domain WHO 1 note: if a user had a WHO 1 impulse cover (cover: with who: "1", where: "0004"), _ForeignAddresses previously only knew "0004", so bus traffic on "04" discovered a light (domain="light", {mac}-1-04) plus {mac}-1-04-disable / {mac}-1-04-enable buttons. Because async_get_entity_id only looks up reg_entry.domain ("cover"), the device merge below hands light.light_04 and its -disable/-enable buttons onto yaml_dev. light.py's ghost() later removes light.light_04 via registry.async_remove (not prune_entity), leaving {mac}-1-04-disable and {mac}-1-04-enable orphaned on the impulse cover's device (button.py does not create lock/unlock buttons for WHO 1 covers and non_light_addresses in button.py omits WHO 1 covers).

Also worth considering: when duplicate is removed on line 353, if the surviving reg_entry (which never worked on the bus) has area_id is None / empty labels / icon is None, carrying those over from duplicate preserves customizations the user made on the working bus entity.

if device.name_by_user is None and other.name_by_user is not None:
carried["name_by_user"] = other.name_by_user
if carried:
device_registry.async_update_device(device.id, **carried)

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.

Consider carrying labels as well when merging other into device (e.g. if other.labels - device.labels: carried["labels"] = set(device.labels) | set(other.labels)), matching area_id and name_by_user.

_pl = v[_length // 2 :]
if int(_a) >= 0 and int(_a) <= 10 and int(_pl) >= 0 and int(_pl) <= 15:
return f"{_a}{_pl}"
return canonical_point_where(f"{_a}{_pl}") if self.canonical else f"{_a}{_pl}"

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.

Because OWNd.normalize_where("0004") remains "0004" (OWNd#123), two other validation paths in validate.py / where_grammar.py still let WHO 1 / WHO 2 "0004" slip through unchanged:

  1. WHO 1 / WHO 2 expose (_validate_expose_entry & where_grammar.normalize_expose_where, also used by config_flow_expose.py): normalize_expose_where(1, "0004") returns "0004" and is_apl_address("0004") is True, producing a virtual actuator that transmits *1*...*0004## (NACKed by 11/12 gateways), ignores incoming "04" frames, and bypasses duplicate / is_exposed_on_bus checks against "04".
  2. WHO 1 binary_sensor_schema & sensor_schema: Both validate where via SpecialWhere(), so who: "1" motion/illuminance sensors at where: "0004" stay "0004", poll *#1*0004*5/6/7## (NACKed), and fail to match "04" frames.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All items are on the branch (head 79304d9, one commit per item, still draft). A second-opinion review of the diff added two points that are in as well (a canonical expose subentry wins over its 4-digit twin, and the ghost-light cleanup also runs for an already-canonical owner so a retry stays idempotent).

  1. P1 imported myhome.yaml copy (4a40d9f): canonicalise_imported_platforms() moves where, members and the device key (1-0004 → 1-04, #4#II kept) in a new copy stored through the options API; it runs on the read path in _apply_imported_copy() (before the registry migration and the platforms) and as a guard on the write path; a copy already holding the canonical key keeps that device and drops the twin with a WARNING. The churn scenario you described is a test: snapshot with 0004, no myhome.yaml, real registries and the light platform, two boots → one {mac}-1-04, light.kitchen and dimmable kept, no _2, second boot migrates nothing (fails before with two entities).
  2. P2 expose (c13b346): normalize_expose_where() applies the silent helper for WHO 1/2 (0001..0009 → 01..09, #4#II kept, 0000, 0010..0015, WHO 4, WHO 18 untouched); _validate_expose_entry, the yaml list and config_flow_expose.py share expose_entry_schema, so all store 04; existing expose subentries are rewritten (data and title), and when the canonical binding also exists the 4-digit twin is removed with a WARNING. Description item 1 updated.
  3. P2 WHO 1 sensors (b395788): _canonical_who1_point() runs on the whole mapping of binary_sensor_schema / sensor_schema once the WHO default is in (only who: "1"); binary_sensor and sensor joined _POINT_ADDRESS_DOMAINS with the WHO in the unique id as the predicate ({mac}-1-0004-motion moves; {mac}-25-0004, {mac}-4-0004-temperature, {mac}-18-0004-power and WHO-less ids do not).
  4. P2 cover_travel_times (51a7851): 0004 and 0004#4#01 re-keyed through the options API; a record already under the canonical key wins; _stored_calibration() falls back to the 4-digit key. Tested with _stored_calibration.
  5. P3 (b1724ef, 79304d9): silent helpers where_grammar.canonical_point_spelling / canonical_point_address (const.py imports where_grammar, hence that home), so the migration logs no "Write '04' in myhome.yaml"; the WHO 1 impulse-cover ghost light is reproduced by a test (the light and both buttons ended on the cover's device) and the migration now removes this entry's light at the canonical address of a migrated WHO 1 switch or cover with its companion buttons; labels carried on the device merge (union); area_id, icon, name, labels carried from the removed duplicate entity where the survivor has none; the release note mentions the carried fields, the cleanup and the A/PL attribute change (00/04 → 0/4).
  6. Description: #799 (merged) everywhere, base d8bac15, Encyclopedia records added as you listed (r000264, r000450, r000451, r000848 for the shared A/PL family with WHO 2 inferred through it; r000604 / c003426 for WHO 5 as an independent grammar behind PointToPoint(canonical=False)), and a "Review follow-up" table mapping each item to its commit.

Checks: ruff, mypy-strict ratchet (one unreachable removed on the way), HA standards, docs sync, translations; full suite on plain OWNd master 4554 passed, coverage 100 % (16,272 statements); test_canonical_point_where.py 22 → 56 cases with explicit tables for 0000, 0010..0015, 1015, WHO 5/25/9/4/18, #4#II and -disable tails, and idempotence of every migration step.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck please resolve conflicts

@gdluck
gdluck force-pushed the fix/validate-canonical-area0 branch from 79304d9 to 9f5459e Compare October 24, 2026 21:10
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Rebased on the current base (head 9f5459e, mergeable): the only conflicts were in the release-notes file (the merged #796 Covers bullet kept first, this PR's Lighting bullet below it), no code conflicts; all seven commits carry over unchanged. Full suite on plain OWNd master 4564 passed, 100 % coverage; ruff, typing ratchet, HA standards and docs sync clean. The other open drafts of the batch were rebased the same way.

@gdluck
gdluck marked this pull request as ready for review October 24, 2026 21:51
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All review items are addressed and CI is green on the rebased head 9f5459e, so this is out of draft for your re-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 all five items from the previous review (CONF_MIGRATED_YAML_PLATFORMS re-keying, expose normalisation & subentry migration, CONF_COVER_TRAVEL_TIMES re-keying + fallback, cross-domain ghost light + companion button cleanup, and metadata carry-over).

Auditing the updated head (9f5459e2) against the codebase, static-analysis gates, Firmware Oracle, Encyclopedia KB, and committed hardware traces turned up one [P1] bug in the WHO 1 sensor migration, one [P2] linter/standards gate failure, and trace-table / scope cleanups that need to be addressed before merge:

Blocking Code Issues

  1. [P1] WHO 1 motion / illuminance migration checks the wrong bus-duplicate unique_id ({mac}-1-04-* instead of {mac}-04-*)

    • In binary_sensor.py (build_motion sets device_id = ctx.address.where = "04" and MyHOMEMotionSensor builds f"{gateway.mac}-{self._device_id}-{self._attr_device_class}") and sensor.py (build_illuminance sets device_id = primary = "04" and MyHOMEIlluminanceSensor builds f"{gateway.mac}-{self._device_id}-{self._attr_device_class}"), bus-discovered WHO 1 sensors have unique_id {mac}-04-motion and {mac}-04-illuminance (no -1- prefix) — only their DeviceInfo.identifiers carries {mac}-1-04.
    • By contrast, YAML (where: "0004") created {mac}-1-0004-motion / {mac}-1-0004-illuminance, which migrate_canonical_point_addresses renames to {mac}-1-04-motion / {mac}-1-04-illuminance.
    • Because migrate.py only looks up new_unique_id ({mac}-1-04-*), duplicate is always None for real bus-discovered WHO 1 sensors (tests/test_canonical_point_where.py masked this by seeding {MAC}-1-04-motion / {MAC}-1-04-illuminance, which bus discovery never creates):
      1. User customisations (area_id, icon, name, labels) on {mac}-04-motion / {mac}-04-illuminance are never carried over and are wiped when PlatformDiscovery.restore() calls registry.async_remove().
      2. Worse, if {mac}-04-motion appears before {mac}-1-04-motion in er.async_entries_for_config_entry (e.g. when no device handover re-inserted it), binary_sensor.py restore() restores the bus entity {mac}-04-motion first and deletes the user's configured YAML entity {mac}-1-04-motion as the duplicate.
    • Please check for and merge/remove same-entry {mac}-{canonical}-{class} bus twins for WHO 1 binary_sensor / sensor entries (and cover already-renamed {mac}-1-{canonical}-{class} entries on subsequent boots), and update tests/test_canonical_point_where.py to exercise real bus-discovered unique_ids end-to-end through binary_sensor / sensor async_setup_entry.
  2. [P2] ruff check (I001) and scripts/verify_ha_standards.py fail on custom_components/myhome/migrate.py:479

    • The inline import line in _remove_ghost_light exceeds line-length = 100, triggering ruff I001 (Import block is un-sorted or un-formatted) and failing scripts/verify_ha_standards.py ([RULE_RUFF_STATIC_ANALYSIS]).

Trace Backing & Scope Updates

  1. [P2] ## Where are the traces — un-ingested Row 3 and missing committed WHO 1 command & WHO 2 traces

    • Row 3 cites "combined-build re-test of 2026-10-24 (summarised under Testing; raw recordings are not a fixture)". Un-ingested prose summaries belong in ## Hardware Verification, not ## Where are the traces (either move it out of the trace table or ingest the raw fixture).
    • WHO 1 commands: f461_live_2026-10-23 and myhomeserver1_live_2026-10-24 only tested status reads (*#1*000N##), whereas committed tests/fixtures/traces/f461_live_2026-10-15/myhome_trace_F461_live_2026-10-15.json (probe_area0, F461 2.0.11) directly captures WHO 1 command rejections (*1*1*0001## NACK 11 ms, *1*0*0001## NACK 16 ms, *#1*0004## NACK 12 ms vs *1*1*01## / *1*0*01## ACK + bus status). Please cite it in ## Where are the traces.
    • WHO 2 (0001..0009 → 01..09): The PR rewrites WHO 2 point addresses as well, yet ## Where are the traces currently cites zero WHO 2 traces. Please cite the committed physical traces across four gateway families proving gateways report WHO 2 Area-0 PL 1..9 in 2 digits alongside PL 10..15 in 4 digits:
      • tests/fixtures/traces/issue_445/myhome_trace_MyHomeServer1_LN4660M2_2026-09-25T16-47-06.json (MyHomeServer1: WHO 2 02, 03, 04, 08, 09 alongside 0010, 0011 on the same plant)
      • tests/fixtures/traces/issue_466/myhome_sweep_H4890_all_2026-09-26T11-48-10.json (H4890: WHO 2 05, 06, 08)
      • tests/fixtures/traces/issue_619/myhome_sweep_F453AV_all_2026-10-04T19-45-11.json (F453AV: WHO 2 05, 06)
      • tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json (F461 2.0.11: *#2*05## → *2*0*05##)
        Also note explicitly in ## What does the Firmware Oracle / Encyclopedia say that lights-area0 is WHO 1 only and 4-digit WHO 2 polls/commands (*#2*000N## / *2*1*000N##) have not yet been replayed in the oracle.
  2. [P3] Issue #135 (0809 vs 89) scope in release notes / title / docstrings & stale base SHA

    • Issue #135 (#issuecomment-2496228553) is about 0809 vs 89 (Area 8 PL 9), which this PR intentionally leaves as typed (PointToPoint()("0809") == "0809") pending a fleet-wide 0101..0909 sweep. Citing (#147, #135) in .github/release-notes/2.0.0b16.md, the PR title, and the validate.py / migrate.py docstrings implies #135 is resolved here — please drop #135 from those locations (or state explicitly that #135 remains open for non-zero areas 0101..0909).
    • Please also update the stale Based on the current v2-phase1-architecture (d8bac157) line in the PR description when rebasing onto current v2-phase1-architecture.

Comment on lines +359 to +361
new_unique_id = f"{prefix}{new_tail}"
duplicate_id = entity_registry.async_get_entity_id(reg_entry.domain, DOMAIN, new_unique_id)
duplicate = entity_registry.async_get(duplicate_id) if duplicate_id else 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.

[P1] WHO 1 motion / illuminance bus-discovered duplicates use {mac}-{canonical}-{class}, not {mac}-1-{canonical}-{class}.

When bus discovery creates a WHO 1 motion or illuminance entity:

  • binary_sensor.py (build_motion) sets device_id = ctx.address.where ("04"), so MyHOMEMotionSensor sets _attr_unique_id = f"{gateway.mac}-04-motion".
  • sensor.py (build_illuminance) sets device_id = primary ("04"), so MyHOMEIlluminanceSensor sets _attr_unique_id = f"{gateway.mac}-04-illuminance".

Only their DeviceInfo.identifiers includes -1- ({mac}-1-04); the entity unique_id does not. Meanwhile, YAML (where: "0004") created {mac}-1-0004-motion / {mac}-1-0004-illuminance, so new_unique_id here is {mac}-1-04-motion / {mac}-1-04-illuminance.

Looking up only new_unique_id misses the real bus-discovered {mac}-04-motion / {mac}-04-illuminance twin:

  1. Its user customisations (area_id, icon, name, labels) are never carried over before PlatformDiscovery.restore() deletes it via registry.async_remove().
  2. If {mac}-04-motion precedes {mac}-1-04-motion in er.async_entries_for_config_entry (for example when no device handover re-inserted it), binary_sensor.py restore() restores {mac}-04-motion first and deletes the user's configured YAML entity {mac}-1-04-motion as the duplicate.

For WHO 1 binary_sensor / sensor entries (1-{canonical}-{suffix}), please also check for a same-entry bus twin at {mac}-{canonical}-{suffix} (including on subsequent runs where the YAML entity was already renamed to {mac}-1-{canonical}-{suffix}), carry over unset area_id / icon / name / labels, and remove the bus twin.

Comment thread custom_components/myhome/migrate.py Outdated
ghost = entity_registry.async_get(ghost_id) if ghost_id else None
if ghost is None or ghost.config_entry_id != entry.entry_id:
return
from .discovery import COMPANION_SUFFIXES # noqa: PLC0415 # discovery imports myhome_device, which imports this module

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.

[P2] ruff check (I001) and scripts/verify_ha_standards.py fail here.

This line is 121 characters (> 100), which causes ruff (0.16.7) to flag I001 [*] Import block is un-sorted or un-formatted because it wants the inline import wrapped in parentheses, failing scripts/verify_ha_standards.py ([RULE_RUFF_STATIC_ANALYSIS]). Please wrap the import or move the rationale comment onto the preceding line so ruff check custom_components tests scripts and scripts/verify_ha_standards.py pass cleanly.

Comment on lines +584 to +589
bus_motion = ent_reg.async_get_or_create(
"binary_sensor", DOMAIN, f"{MAC}-1-04-motion", config_entry=config_entry, device_id=bus_dev.id
)
bus_lux = ent_reg.async_get_or_create(
"sensor", DOMAIN, f"{MAC}-1-04-illuminance", config_entry=config_entry, device_id=bus_dev.id
)

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.

[P1] Synthetic unique_ids mask the WHO 1 bus-duplicate bug.

Bus discovery (build_motion in binary_sensor.py and build_illuminance in sensor.py) never produces {MAC}-1-04-motion or {MAC}-1-04-illuminance — it produces {MAC}-04-motion and {MAC}-04-illuminance (while attaching them to bus_dev {MAC}-1-04). The same applies to bus_sensor at lines 186 and 270.

Please replace these with the real bus-discovered unique_ids (f"{MAC}-04-motion" / f"{MAC}-04-illuminance"), verify metadata carry-over (area_id, icon, name, labels), and add an end-to-end test through binary_sensor and sensor async_setup_entry (including when the bus entry precedes the YAML entry in the entity registry).

Comment thread .github/release-notes/2.0.0b16.md Outdated
- **Expose Home Assistant to the bus**: a declarative `expose` engine projects HA entities and the clock onto free SCS addresses: virtual WHO 1 / WHO 2 actuators that a wall control can drive (mirroring HA state back to its LEDs), WHO 4 virtual temperature probes, WHO 18 virtual energy meters and the WHO 13 bus clock. Custom actions run as Home Assistant scripts (#750).
- **Covers**: `class:` in `myhome.yaml` now reaches WHO 2 covers (a `curtain` is no longer shown as a shutter; #39); Venetian blind slat tilt as timed slat pulses (`slat_tilt`, `slat_time`) (#508); impulse garage and gate covers on WHO 1 relays with optional safety approval, door-entry strike locks and doorbell events (#667); native broadcast for general covers behind an interface (#679); a cover stop overtakes other queued frames (#694); cover kinematics split into pluggable strategies (#739).
- **Lighting**: 2-digit Area 0 point addresses `01`–`09` (#713); ON dimmers on a fresh install are probed for brightness support (#742); timed turn-on uses the OWNd builders (#659); lights the startup sweep already reported are not polled again (#701); area and group re-syncs only skip once every member has echoed, counting only the members' own echoes in the commanded state (#740, #749).
- **Lighting**: 2-digit Area 0 point addresses `01`–`09` (#713); `where: "0004"` in `myhome.yaml` is resolved to `04`, the spelling the gateways accept and report, with a one-time warning, and registry entries created under the 4-digit spelling are moved to it; the duplicate entity and device that bus discovery had created under `04` are removed, their area, labels, icon and custom name carried over to the configured ones where those have none, and a light that bus discovery had created at the address of a WHO 1 switch or impulse cover goes with its lock / unlock buttons; the `A` / `PL` state attributes of such a light, switch or cover read `0` / `4` instead of `00` / `04`, as on bus-discovered entities (#147, #135); ON dimmers on a fresh install are probed for brightness support (#742); timed turn-on uses the OWNd builders (#659); lights the startup sweep already reported are not polled again (#701); area and group re-syncs only skip once every member has echoed, counting only the members' own echoes in the commanded state (#740, #749).

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.

[P3] Drop #135 here (or clarify that 0809 / 0101..0909 is unchanged).

Issue #135 (#issuecomment-2496228553) is specifically about 0809 vs 89 (Area 8 PL 9), which this PR leaves as typed (PointToPoint()("0809") == "0809"). Citing (#147, #135) in the release note (and PR title / validate.py / migrate.py docstrings) implies #135 is fixed in 2.0.0b16 when only Area-0 #147 (0001..0009 → 01..09) is rewritten.

@gdluck
gdluck force-pushed the fix/validate-canonical-area0 branch from 9f5459e to f764154 Compare October 25, 2026 11:59
@gdluck gdluck changed the title fix(validate): canonicalise 4-digit area-0 point addresses (0004 → 04) (#147, #135) fix(validate): canonicalise 4-digit area-0 point addresses (0004 → 04) (#147) Oct 25, 2026
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks for the audit; all four are on the branch (head f764154, rebased on 089fef5, the seven earlier commits unchanged in content).

  1. [P1] WHO 1 motion / illuminance bus twins (5fb1f18): confirmed, build_motion / build_illuminance give {mac}-04-motion / {mac}-04-illuminance with the WHO only in DeviceInfo.identifiers. migrate.py gains _merge_bus_sensor_twin(): for a WHO 1 binary_sensor / sensor entity at an area-0 point it looks up this entry's {mac}-{canonical}-{suffix} twin both after the rename and when the YAML entity is already canonical (later boots, or YAML saying 04), carries over unset area_id / icon / name / labels through a shared helper that re-fetches the survivor so a second carry never overwrites, and removes the twin; another entry's twin is untouched; a second run makes no registry write (pinned by patching every registry write to raise). The tests now seed the real bus unique ids (including the light tests' hanging sensor) and run end to end through binary_sensor and sensor async_setup_entry with the twin listed first. One precision on your consequence 2: in the plain first boot the device hand-over re-lists the twin last, so the pre-fix tree lost the customisations but kept the user's entity; the order that removed binary_sensor.hall_motion is the one without a hand-over (both already on {mac}-1-04), reproduced by test_motion_sensor_setup_keeps_an_already_canonical_yaml_entity_listed_after_its_twin. Five new / rewritten tests fail before the fix.
  2. [P2] ruff I001 (bd9de12): reproduced with ruff 0.16.7 (0.17.2 in my venv did not flag it, which is how it slipped); the rationale comment moved above the import; both versions and verify_ha_standards.py pass.
  3. [P2] Trace table: the combined re-test row moved to Hardware Verification; f461_live_2026-10-15 probe_area0 (*1*1*0001## NACK 11 ms, *1*0*0001## NACK 16 ms, *#1*0004## NACK 12 ms against *1*1*01## / *1*0*01## ACK with their events) and the four WHO 2 traces (issue_445 MyHomeServer1 / LN4660M2 02 03 04 08 09 beside 0010 / 0011, issue_466 H4890 05 06 08, issue_619 F453AV 05 06, f461_live_2026-10-23 *#2*05## → *2*0*05##) are cited with the frames as they are in the fixtures. The oracle section states that lights-area0 is WHO 1 only and that 4-digit WHO 2 polls / commands have not been replayed (no such stimulus in results/ at 52d354b).
  4. [P3] My home stopping Update #135: dropped from the release note, the PR title and the validate.py / migrate.py docstrings, which now say the 0809 of My home stopping Update #135 is a point of area 8 and stays as typed, so My home stopping Update #135 remains open for 0101..0909; the base line reads 089fef5. The first commit's subject still carries (#147, #135); say if you want it reworded.

4620 passed / 1 skipped (base 4559), 100 % coverage; ruff (0.16.7 and 0.17.2), mypy --strict, HA standards, docs sync and translations clean. An independent second-opinion review of the diff asked that the idempotence tests assert no registry writes rather than only logs; done.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

@gdluck please fix release notes

gdluck and others added 5 commits October 25, 2026 22:21
…t-HA#147, OpenWebNet-HA#135)

`where: "0004"` in myhome.yaml built unique ids and frames with the
4-digit spelling. The gateways refuse it (`*#1*0001##` and `*1*1*0001##`
refused on 11 of 12 emulated models, published oracle `lights-area0`;
`*#1*0007##` NACKed by a physical F461 2.0.11 while `*#1*07##` is
answered) and report the point as `04`, so the configured light never
sent a working command or saw its own status, and bus discovery created
a second entity under `04`.

`canonical_point_where()` rewrites a 4-digit area-0 point with PL 1..9
to its 2-digit form with one WARNING per address; every other 4-digit
input (`0015`, `0209`, `1015`) stays as typed, as OWNd does (OWNd#123).
`PointToPoint` applies it for the light, switch, cover and members
WHERE; the alarm schema keeps the raw input (WHO 5 is not a point).
`migrate_canonical_point_addresses()` moves entity unique ids and device
identifiers from `-1-0004…` to `-1-04…`, merging a bus-created duplicate
into the user's entity, which keeps its entity_id, name and area.

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

`migrate_canonical_point_addresses()` called `canonical_point_where()`,
which logs "Write '04' in myhome.yaml to silence this warning" once per
address. During a registry migration that warning is wrong: the user may
already have written '04', or never had a myhome.yaml (imported copy).

`canonical_point_spelling()` is the pure rewrite (`0004` -> `04`, anything
else as typed) and `canonical_point_address()` applies it in front of an
optional `michnovka#4#II` suffix. Both live in where_grammar.py, the module every
address consumer may import (const.py imports it, so validate.py cannot
be their home); `canonical_point_where()` in validate.py keeps the
one-shot WARNING for the yaml path. migrate.py uses the silent helper.

Tests: the silent helpers' table (`0000`, `0010`..`0015`, `1015`, `michnovka#4#II`
kept; `0004`, `0009#4#01` rewritten; no log record); the `2-0009#4#01`
rename logs no WARNING and leaves the yaml warning set untouched
(asserted exactly one warning before).

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

A copy of myhome.yaml imported into the entry options by an earlier beta
(OpenWebNet-HA#733) holds `where: "0004"` under the key `1-0004`. With myhome.yaml
gone, `_apply_imported_copy()` applied it as stored, bypassing the
validator: the registry migration renamed `{mac}-1-0004` to `{mac}-1-04`,
`restore()` found no configuration for `04` (losing `dimmable`,
`lock_features`, `rgb`, `model`) and `configure()` re-created `0004` as
`light.<name>_2`; the next boot deleted `04` as the duplicate and renamed
again, churning `_2`, `_3`, ... on every restart.

`canonicalise_imported_platforms()` returns a new copy with the WHO 1 /
WHO 2 `where`, `members` and device keys in the canonical spelling (the
`michnovka#4#II` key suffix kept); a copy already holding the canonical key keeps
that device and drops the 4-digit twin with a WARNING. The read path
applies it before the copy is used and stores the result through the
options API; the write path applies it as a guard. Other WHO (CEN at
`0004`, WHO 5 zone `0004`) and non-point platforms are untouched.

Tests: the rewrite table (light, button alias, routed cover, members;
WHO 25 / WHO 5 / `0015` untouched; the stored dict not mutated;
idempotent), the collision case, the write-path guard, and the two-boot
scenario (snapshot with `0004`, no myhome.yaml, real registries and the
light platform): one `{mac}-1-04` entity that keeps `light.kitchen`, its
device and `dimmable`, no `_2`, nothing migrated on the second boot.
All four fail before the change.

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

`normalize_expose_where()` delegated WHO 1 / WHO 2 to `OWNd.normalize_where()`,
which keeps `0004` as typed (OWNd#123), and `_validate_expose_entry()` accepted
it because `is_apl_address("0004")` is True. The virtual actuator then sent
`*1*...*0004##` (refused by 11 of 12 gateway models), never matched the wall
switch's `04` frames and escaped the duplicate-binding and `is_exposed_on_bus`
checks against `04`.

For WHO 1 / WHO 2 the grammar now applies the silent `canonical_point_address()`
after `normalize_where()`: `0001`..`0009` become `01`..`09` with the `michnovka#4#II`
suffix kept, `0000` and `0010`..`0015` stay as typed, WHO 4 / WHO 18 keep their
own grammars. `_validate_expose_entry()`, the yaml `expose:` list and
`config_flow_expose.py` all go through it (shared `expose_entry_schema`).
`migrate_canonical_point_addresses()` rewrites existing `expose`
ConfigSubentries holding such an address (data and title) through the
subentry API; load-time normalisation stays as the guard.

Tests: the grammar table (9 cases, no log record), the shared schema and the
resulting binding key, and the subentry migration (two moved, `0015`, a WHO 4
probe and the clock untouched, second run silent). Four fail before.

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

`binary_sensor_schema` (motion) and `sensor_schema` (illuminance) validated
`where` with `SpecialWhere()`, which sees one field and cannot know the WHO,
so a WHO 1 sensor at `where: "0004"` kept `0004` (key `1-0004`), polled
`*#1*0004*5/6/7##` (refused by 11 of 12 gateway models), missed the `04`
frames and let bus discovery create `{mac}-1-04-motion` /
`{mac}-1-04-illuminance` next to it.

`_canonical_who1_point()` runs on the whole device mapping once the WHO
default is in: only `who: "1"` is rewritten (one yaml WARNING per address,
as for lights); WHO 25 / 9 / 4 / 18 keep their grammar.
`migrate_canonical_point_addresses()` covers the `binary_sensor` and
`sensor` domains; the WHO in the unique id is the predicate
(`{mac}-1-0004-motion` moves, `{mac}-25-0004`, `{mac}-4-0004-temperature`,
`{mac}-18-0004-power` and the WHO-less legacy `{mac}-0004-illuminance`
do not match).

Tests: both schemas (`0004` -> `04`, `0005` -> `05`, warnings), the other
WHO and `0015` untouched, and the registry case with bus twins removed,
the twin device merged and four other-WHO entries untouched. Two of the
three fail before. Sensor and binary sensor docs name the spelling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck and others added 5 commits October 25, 2026 22:21
…dress (OpenWebNet-HA#147)

`entry.options["cover_travel_times"]` is keyed by the cover's device id
(`0004`, `0004#4#01`). Once the cover is addressed as `04` / `04#4#01`,
`_stored_calibration(entry, "04")` returned None and the measured travel
times or the `slat_time` override were silently lost.

`_migrate_cover_travel_times()` moves such records to the canonical key
through the options API during `migrate_canonical_point_addresses()`. A
record already stored under the canonical key belongs to the cover that
worked on the bus and wins; the 4-digit one is dropped with an INFO line.
`_stored_calibration()` also falls back to the 4-digit spelling, for a
record a migration could not move.

Tests: the re-keying (plain and routed keys moved, `0015` and `21`
untouched, the `0005` / `05` collision resolved for `05`, second run
writes nothing) and the read fallback. Both fail before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ver, carry labels, icon and area (OpenWebNet-HA#147)

A WHO 1 impulse cover (or switch) declared as `0004` only knew that
spelling, so frames on `04` discovered a light there with its lock /
unlock buttons. Unique ids are scoped per domain, so the duplicate lookup
of the migration did not see that light; the device merge handed it and
the buttons to the cover's device, and light.py later removed the light
with `registry.async_remove`, leaving `…-1-04-disable` / `-enable`
orphaned on the impulse cover (reproduced by the new test before the
fix: the light and both buttons sat on the cover's device). The migration
now removes this entry's light at the canonical address of a migrated
WHO 1 switch or cover together with its companion buttons; a light of
another config entry is left alone.

On the merges, the twin's `labels` are carried onto the surviving device
(union), and the removed duplicate entity's `area_id`, `icon`, `name` and
`labels` are carried onto the surviving entity where it has none; what
the survivor already has wins (pinned by a test).

The release note names the carried fields, the ghost-light cleanup and
that the `A` / `PL` state attributes of a light, switch or cover declared
as `0004` now read `0` / `4` instead of `00` / `04`, as on bus-discovered
entities.

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

Bus discovery spells a WHO 1 motion or illuminance sensor without the WHO
(`{mac}-04-motion`, `{mac}-04-illuminance`: binary_sensor.py `build_motion`,
sensor.py `build_illuminance`); only its device identifier carries `{mac}-1-04`.
The migration looked the duplicate up under the yaml spelling `{mac}-1-04-motion`
only, which bus discovery never creates, so the twin was left to the platform's
restore: whichever entry the registry listed first survived and the other was
removed as a second entry for the same address, and the twin's area, icon, name
and labels were lost in every case.

For a WHO 1 binary_sensor / sensor entity at an area-0 point the migration now
also looks up this entry's `{mac}-{canonical}-{suffix}` twin, after the rename
and when the yaml entity is already canonical (a later boot, or a yaml that said
`04` from the start), carries its unset area, icon, name and labels onto the
yaml entity and removes it, so the restore order no longer decides. A twin of
another config entry is left alone; a second run finds nothing to do.

Tests seed the real bus unique ids; the two end-to-end cases run through
`binary_sensor` / `sensor` `async_setup_entry` with the twin listed before the
yaml entity, including the order that removed the user's entity before the fix.
Fail-before on the previous commit: 5 of the 5 new / rewritten tests.

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

ruff 0.16.7 reports I001 on the 124-character inline import in
`_remove_ghost_light` (it wants the import wrapped), which fails
`scripts/verify_ha_standards.py`; the rationale comment now sits on the line
above and the import fits. ruff 0.17.2 did not flag it, which is how it slipped
through the local check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s 0809 is a non-zero area and stays as typed

this change leaves as typed (`PointToPoint()("0809") == "0809"`), pending a
fleet-wide `0101`..`0909` sweep. The release note, the `canonical_point_where`
and `migrate_canonical_point_addresses` docstrings and the test module no
longer cite OpenWebNet-HA#135 as resolved; the release note and the validator docstring say
why it stays open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Release notes fixed: rebased onto v2-phase1-architecture d465db8 (after #792, #793, #797 and #815), so .github/release-notes/2.0.0b16.md now carries this PR's clause inside the current Lighting bullet only and leaves the merged Thermoregulation (WHO 4) bullet as it is on the base (the conflict came from the three merged PRs touching the same two bullets). Head fd763df, mergeable; 4854 passed / 1 skipped at 100 % coverage, ruff, typing ratchet, HA standards and docs sync clean.

@gdluck
gdluck force-pushed the fix/validate-canonical-area0 branch from f764154 to fd763df Compare October 25, 2026 20:24

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

All 4 items from review #5642942172 are cleanly resolved:

  1. WHO 1 motion / illuminance bus twins: _merge_bus_sensor_twin() handles {mac}-0[1-9]…-<suffix> both after renaming 1-0004-… and when 1-04-… is already canonical, carrying unset area_id, icon, name, and labels and removing the same-entry twin independent of registry order, backed by twin-first end-to-end binary_sensor / sensor setup tests.
  2. Lint: ruff I001 in _remove_ghost_light() resolved (0 violations).
  3. Traces & Oracle provenance: Un-ingested combined-build live retest moved to Hardware Verification; all 7 rows in Where are the traces verified against the committed fixtures (f461_live_2026-10-23, myhomeserver1_live_2026-10-24, f461_live_2026-10-15, issue_445, issue_466, issue_619); WHO 2 oracle replay gap documented.
  4. Scope (#135): Dropped from title, release note, and docstrings with explicit note that area-8 0809 stays as typed and #135 remains open.

Verified merged cleanly onto v2-phase1-architecture (b48e774e) against pinned OWNd==2.0.0b11: 4,927 passed, 100.0% statement coverage (16,660 / 16,660 across 73 modules), mypy --strict (0 errors), verify_ha_standards.py, sync_documentation.py --check, and manage_translations.py check all green. Thanks for the thorough follow-ups!

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit bc8f729 into OpenWebNet-HA:v2-phase1-architecture Oct 25, 2026
17 checks passed
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 26, 2026
…re canonicalised (OpenWebNet-HA#798 follow-up)

canonicalise_imported_platforms appended "plat: old -> new" for every
rewritten device, also when the key was unchanged and only the `members`
list of a scope cover held a 4-digit area-0 address: the INFO line then read
"1 point address(es) moved to the canonical spelling (cover: 2-1 -> 2-1)",
a move that moved nothing, and dropped what did change. The line now reads
"cover: 2-1 members ['0004', '12'] -> ['04', '12']" for that case; a moved
key is reported as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit that referenced this pull request Oct 26, 2026
…e-arg-and-log

fix(config_flow,legacy_yaml): drop an inert profile argument, say what moved, no unreachable MAC fallback (#804 / #798 / #801 / #808 follow-up)
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