Repository navigation
fix(validate): canonicalise 4-digit area-0 point addresses (0004 → 04) (#147) - #798
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…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>
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>
5033698 to
2336301
Compare
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>
f673b6c to
71bed53
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
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
-
P1 — Imported
myhome.yamlsnapshot (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()storedentry.options["migrated_yaml_platforms"]with"where": "0004"and raisedlegacy_yaml_importedtelling usersmyhome.yamlcan be deleted. - When
myhome.yamlis gone,_apply_imported_copy()loads that stored snapshot directly via_apply_yaml_platforms()without passing throughvalidate.config_schema(). - On boot: (a)
migrate_canonical_point_addresses()renames{mac}-1-0004→{mac}-1-04; (b)PlatformDiscovery.restore()restores{mac}-1-04withcfg={}becauseconfig_for()misses"04"inconfigured_platforms(losingdimmable,lock_features,rgb,color_temp,model), recording only"04"inself.known; (c)PlatformDiscovery.configure()sees"0004" not in self.knownand re-creates{mac}-1-0004(light.<name>_2). On every subsequent restart,migrate_canonical_point_addresses()deletes{mac}-1-04as a "duplicate" and renames{mac}-1-0004→{mac}-1-04, churninglight.<name>_2→_3→_4on every boot. - Action: Canonicalise point
wherevalues (and re-key aliases) inentry.options[CONF_MIGRATED_YAML_PLATFORMS]during migration and/or inside_apply_imported_copy().
- In 2.0 betas after
-
P2 —
expose(myhome.yamland UIconfig_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 toOWNd.normalize_where(raw)("0004") and_validate_expose_entry()(shared by YAMLexpose:andconfig_flow_expose.py) accepts"0004"becauseis_apl_address("0004")isTrue. - 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..09for WHO 1/2 innormalize_expose_where()(preserving optional#4#IIinterface suffix) and migrate any existingexposeConfigSubentriesholding0001..0009.
- Contrary to item 1 of
-
P2 — WHO 1
binary_sensor(motion) andsensor(illuminance) are neither canonicalised nor migrated:binary_sensor_schema(who: "1") andsensor_schema(who: "1",class: illuminance) validatewhereviaSpecialWhere()rather than canonicalising WHO 1 4-digit area-0 points, and_POINT_ADDRESS_DOMAINSinmigrate.pyomits"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
wherewhenwho == "1"inbinary_sensor_schema/sensor_schemaand include"binary_sensor"and"sensor"inmigrate_canonical_point_addresses().
-
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 bydevice_id("0004"or"0004#4#01"). After migrating a cover to"04"/"04#4#01",_stored_calibration(entry, "04")returnsNone, silently losing measured travel times orslat_timeoverrides.- Action: Re-key
0001..0009(with optional#4#II) inentry.options[CONF_COVER_TRAVEL_TIMES]during migration.
-
P3 — Minor migration & logging cleanups (see inline comments):
- Split a silent helper from
canonical_point_where()somigrate.pydoes not log "Write '04' in myhome.yaml to silence this warning" whenmyhome.yamlis already updated or absent. - Handle cross-domain WHO 1 ghost
light+ orphaned-disable/-enablecompanion buttons when a WHO 1 impulsecover(who: "1",where: "0004") is migrated. - Carry
labelson device merge (and consider carryingarea_id/labels/iconfrom a removedduplicateentity when the surviving YAML entity has none), and note in the2.0.0b16release note if relevant thatlight/switchstate attributesA/PLchange from"00"/"04"to"0"/"4"(matching bus-discovered entities).
- Split a silent helper from
PR Description (#issue-6013207487) Updates Needed
- Contradictory
#799status:## Hardware Verificationsayscaptures in MyHOME #799 (merged), whereas row 1 of## Where are the tracesstill saystrace PR: #799 (open)(#799merged asad8431ff). - Stale base SHA (
ddc6d601): Both## What is the problemand## How is it testedstill citev2-phase1-architecture (ddc6d601)after the rebase ontod8bac157. config_flow_expose.pyclaim: Update item 1 of## What is the solutiononce WHO 1/2exposecanonicalisation is wired in.- Encyclopedia provenance in
## What does the Firmware Oracle / Encyclopedia say: Add the OpenWebNet Encyclopedia Machine KB records alongside thelights-area0oracle table — specificallyownkb:chunk:r000264,r000450,r000451,r000848(shared SCSA/PLfamily for WHO 1 & WHO 2, noting WHO 2 is inferred via sharedA/PLgrammar rather than a separate WHO 2 oracle stimulus) andownkb:chunk:r000604/ownkb:claim:c003426(WHO 5 alarm addressing is an independent grammar, justifyingPointToPoint(canonical=False)onalarm_control_panel_schema).
| pass | ||
|
|
||
| try: | ||
| migrate_canonical_point_addresses(hass, entry) |
There was a problem hiding this comment.
Two config-entry options stores also hold 4-digit area-0 point spellings and need migration alongside the entity/device registries (plus expose subentries):
- P1 —
entry.options["migrated_yaml_platforms"](CONF_MIGRATED_YAML_PLATFORMSinlegacy_yaml.py): If a user'smyhome.yamlwas imported by#733in an earlier 2.0 beta andmyhome.yamlwas then deleted,_apply_imported_copy()loads the stored snapshot withoutvalidate.config_schema(). Aftermigrate_canonical_point_addresses()renames{mac}-1-0004→{mac}-1-04,PlatformDiscovery.restore()missesconfig_for(..., "04")(droppingdimmable/lock_features/rgb/model) andPlatformDiscovery.configure()immediately re-creates{mac}-1-0004(light.<name>_2), causing an entity-deletion/re-creation churn loop on every restart. - P2 —
entry.options["cover_travel_times"](CONF_COVER_TRAVEL_TIMESincover_calibration.py): Keyed bydevice_id("0004"/"0004#4#01"). Once the cover moves to"04"/"04#4#01",_stored_calibration(entry, "04")returnsNone, orphaning calibrated travel times andslat_timeoverrides.
|
|
||
|
|
||
| # 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") |
There was a problem hiding this comment.
_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.
| """``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"]) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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}" |
There was a problem hiding this comment.
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:
- WHO 1 / WHO 2
expose(_validate_expose_entry&where_grammar.normalize_expose_where, also used byconfig_flow_expose.py):normalize_expose_where(1, "0004")returns"0004"andis_apl_address("0004")isTrue, producing a virtual actuator that transmits*1*...*0004##(NACKed by 11/12 gateways), ignores incoming"04"frames, and bypasses duplicate /is_exposed_on_buschecks against"04". - WHO 1
binary_sensor_schema&sensor_schema: Both validatewhereviaSpecialWhere(), sowho: "1"motion/illuminance sensors atwhere: "0004"stay"0004", poll*#1*0004*5/6/7##(NACKed), and fail to match"04"frames.
|
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).
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); |
|
@gdluck please resolve conflicts |
79304d9 to
9f5459e
Compare
|
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. |
|
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
left a comment
There was a problem hiding this comment.
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
-
[P1]WHO 1motion/illuminancemigration checks the wrong bus-duplicateunique_id({mac}-1-04-*instead of{mac}-04-*)- In
binary_sensor.py(build_motionsetsdevice_id = ctx.address.where = "04"andMyHOMEMotionSensorbuildsf"{gateway.mac}-{self._device_id}-{self._attr_device_class}") andsensor.py(build_illuminancesetsdevice_id = primary = "04"andMyHOMEIlluminanceSensorbuildsf"{gateway.mac}-{self._device_id}-{self._attr_device_class}"), bus-discovered WHO 1 sensors haveunique_id{mac}-04-motionand{mac}-04-illuminance(no-1-prefix) — only theirDeviceInfo.identifierscarries{mac}-1-04. - By contrast, YAML (
where: "0004") created{mac}-1-0004-motion/{mac}-1-0004-illuminance, whichmigrate_canonical_point_addressesrenames to{mac}-1-04-motion/{mac}-1-04-illuminance. - Because
migrate.pyonly looks upnew_unique_id({mac}-1-04-*),duplicateis alwaysNonefor real bus-discovered WHO 1 sensors (tests/test_canonical_point_where.pymasked this by seeding{MAC}-1-04-motion/{MAC}-1-04-illuminance, which bus discovery never creates):- User customisations (
area_id,icon,name,labels) on{mac}-04-motion/{mac}-04-illuminanceare never carried over and are wiped whenPlatformDiscovery.restore()callsregistry.async_remove(). - Worse, if
{mac}-04-motionappears before{mac}-1-04-motioniner.async_entries_for_config_entry(e.g. when no device handover re-inserted it),binary_sensor.pyrestore()restores the bus entity{mac}-04-motionfirst and deletes the user's configured YAML entity{mac}-1-04-motionas the duplicate.
- User customisations (
- Please check for and merge/remove same-entry
{mac}-{canonical}-{class}bus twins for WHO 1binary_sensor/sensorentries (and cover already-renamed{mac}-1-{canonical}-{class}entries on subsequent boots), and updatetests/test_canonical_point_where.pyto exercise real bus-discoveredunique_ids end-to-end throughbinary_sensor/sensorasync_setup_entry.
- In
-
[P2]ruff check(I001) andscripts/verify_ha_standards.pyfail oncustom_components/myhome/migrate.py:479- The inline import line in
_remove_ghost_lightexceedsline-length = 100, triggeringruffI001(Import block is un-sorted or un-formatted) and failingscripts/verify_ha_standards.py([RULE_RUFF_STATIC_ANALYSIS]).
- The inline import line in
Trace Backing & Scope Updates
-
[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-23andmyhomeserver1_live_2026-10-24only tested status reads (*#1*000N##), whereas committedtests/fixtures/traces/f461_live_2026-10-15/myhome_trace_F461_live_2026-10-15.json(probe_area0, F4612.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 tracescurrently 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 202, 03, 04, 08, 09alongside0010, 0011on the same plant)tests/fixtures/traces/issue_466/myhome_sweep_H4890_all_2026-09-26T11-48-10.json(H4890: WHO 205, 06, 08)tests/fixtures/traces/issue_619/myhome_sweep_F453AV_all_2026-10-04T19-45-11.json(F453AV: WHO 205, 06)tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json(F4612.0.11:*#2*05##→*2*0*05##)
Also note explicitly in## What does the Firmware Oracle / Encyclopedia saythatlights-area0is WHO 1 only and 4-digit WHO 2 polls/commands (*#2*000N##/*2*1*000N##) have not yet been replayed in the oracle.
- Row 3 cites
-
[P3]Issue#135(0809vs89) scope in release notes / title / docstrings & stale base SHA- Issue
#135(#issuecomment-2496228553) is about0809vs89(Area 8 PL 9), which this PR intentionally leaves as typed (PointToPoint()("0809") == "0809") pending a fleet-wide0101..0909sweep. Citing(#147, #135)in.github/release-notes/2.0.0b16.md, the PR title, and thevalidate.py/migrate.pydocstrings implies#135is resolved here — please drop#135from those locations (or state explicitly that#135remains open for non-zero areas0101..0909). - Please also update the stale
Based on the current v2-phase1-architecture (d8bac157)line in the PR description when rebasing onto currentv2-phase1-architecture.
- Issue
| 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 |
There was a problem hiding this comment.
[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) setsdevice_id = ctx.address.where("04"), soMyHOMEMotionSensorsets_attr_unique_id = f"{gateway.mac}-04-motion".sensor.py(build_illuminance) setsdevice_id = primary("04"), soMyHOMEIlluminanceSensorsets_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:
- Its user customisations (
area_id,icon,name,labels) are never carried over beforePlatformDiscovery.restore()deletes it viaregistry.async_remove(). - If
{mac}-04-motionprecedes{mac}-1-04-motioniner.async_entries_for_config_entry(for example when no device handover re-inserted it),binary_sensor.pyrestore()restores{mac}-04-motionfirst and deletes the user's configured YAML entity{mac}-1-04-motionas 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.
| 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 |
There was a problem hiding this comment.
[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.
| 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 | ||
| ) |
There was a problem hiding this comment.
[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).
| - **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). |
There was a problem hiding this comment.
[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.
9f5459e to
f764154
Compare
0004 → 04) (#147, #135)0004 → 04) (#147)
|
Thanks for the audit; all four are on the branch (head f764154, rebased on 089fef5, the seven earlier commits unchanged in content).
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. |
|
@gdluck please fix release notes |
…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>
…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>
|
Release notes fixed: rebased onto |
f764154 to
fd763df
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
All 4 items from review #5642942172 are cleanly resolved:
- WHO 1
motion/illuminancebus twins:_merge_bus_sensor_twin()handles{mac}-0[1-9]…-<suffix>both after renaming1-0004-…and when1-04-…is already canonical, carrying unsetarea_id,icon,name, andlabelsand removing the same-entry twin independent of registry order, backed by twin-first end-to-endbinary_sensor/sensorsetup tests. - Lint:
ruffI001in_remove_ghost_light()resolved (0violations). - Traces & Oracle provenance: Un-ingested combined-build live retest moved to
Hardware Verification; all 7 rows inWhere are the tracesverified 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. - Scope (
#135): Dropped from title, release note, and docstrings with explicit note that area-80809stays as typed and#135remains 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!
bc8f729
into
OpenWebNet-HA:v2-phase1-architecture
Requested by A
What is the problem
where: "0004"inmyhome.yamlpassesPointToPointas typed, so the light (or switch, cover, member) gets the unique id…-1-0004and 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 as04, 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. The0809vs89case 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-wide0101..0909sweep, 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 keeps0004as 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: newcanonical_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 inmyhome.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).PointToPointapplies it, so the light / switch / cover WHERE, the cover and lightmembersand the impulse-cover check all resolve to the canonical form; the alarm schema passescanonical=False(WHO 5 addressing is an independent grammar, see the Encyclopedia records below).where_grammar.pyholds the pure helperscanonical_point_spelling()/canonical_point_address()(the#4#IIsuffix kept) for every stored spelling;normalize_expose_where()applies them for WHO 1 / WHO 2, so the yamlexpose:list and the UI expose flow (config_flow_expose.py, sharedexpose_entry_schema) store04for0004and the virtual actuator answers on the address the wall switch uses;binary_sensor_schema/sensor_schemacanonicalisewherewhenwho == "1"(motion / illuminance) on the whole mapping, sinceSpecialWhere()cannot see the WHO.migrate.py:migrate_canonical_point_addresses()runs at the end ofmigrate_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:…-04entity held by another config entry is a collision, nothing is touched and a WARNING names both entities;area_idandname_by_userare carried over to the surviving device (only where the surviving device has none);binary_sensorandsensordomains are covered; the WHO in the unique id is the predicate ({mac}-1-0004-motionmoves,{mac}-25-0004,{mac}-4-0004-temperature,{mac}-18-0004-powerand the WHO-less legacy{mac}-0004-illuminancedo not match);exposeConfigSubentries holding0001..0009(WHO 1 / WHO 2,#4#IIkept) 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;0004(unique ids are scoped per domain, so the same-domain lookup never saw it) is removed together with its-disable/-enable/-calibratebuttons, 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;labelsare carried (union) and the removed duplicate entity'sarea_id,icon,nameandlabelsare carried onto the surviving entity where it has none (what the survivor has wins);{mac}-04-motion,{mac}-04-illuminance:binary_sensor.pybuild_motion,sensor.pybuild_illuminance; only the device identifier carries{mac}-1-04), so the same-domain lookup under{mac}-1-04-motionnever found them and the platform's restore decided by registry order which of the two entries for04survived. For abinary_sensor/sensorentity 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 said04from the start), carries its unsetarea_id/icon/name/labelsand removes it; a twin of another config entry is left alone;validate.py, where the user can act on it.legacy_yaml.py:canonicalise_imported_platforms()moves the copy ofmyhome.yamlstored inentry.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,membersand the device key (1-0004->1-04, the#4#IIsuffix 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-created0004aslight.<name>_2, and every later boot churned_2->_3-> ... (review item 1).A/PLstate attributes of a light, switch or cover declared as0004now read0/4instead of00/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.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 }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 oneWARNING … WHERE '0007' is the 4-digit spelling of the point A=0 PL=7; using '07' …at startup;light.light_2kept its unique id…-1-07(no duplicate entity),tx *1*1*07##/rx *1*1*07##andtx *1*0*07##/rx *1*0*07##were written and followed; the yaml was restored afterwards. The registry merge of a pre-existing…-1-0004entity 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, including0209,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#01kept,0004/0009#4#01rewritten, no log record); yaml device keys use the canonical spelling (1-04,2-05); a light configured as0004is set up once with unique id…-1-04and itsis_onfollows*1*1*04##through the dispatcher without a duplicate being discovered; the registry migration moves…-1-0004/…-1-0004-disableto…-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-0015andlight.kitchen, and a second run changes nothing; the survivor's own customisations win; a…-2-0009#4#01cover and its device are renamed without a WARNING; registryValueErrors are logged and leave the ids untouched; a refused rename puts the bus twin back; a…-04entity of another config entry is a collision.members; WHO 25 / WHO 5 /0015untouched; the stored dict not mutated; idempotent), the collision case, the write-path guard, and the two-boot scenario: a snapshot with0004, nomyhome.yaml, real entity and device registries and the light platform; both boots give one…-1-04light that keepslight.kitchen, its device anddimmable, no_2, and the second boot migrates nothing.expose_entry_schemaand the resulting binding key, the subentry migration (two moved,0015, a WHO 4 probe, the clock and a non-expose subentry untouched, the0007/07collision resolved for07with a WARNING, second run silent).0004->04, warnings), WHO 25 / 9 / 4 / 18 and0015untouched; 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-motionof 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 (everyEntityRegistry/DeviceRegistrywrite patched to raise); the already-canonical case ({mac}-1-04-motionnext to{mac}-04-motion, routed09#4#01too;1-15-motion,0015, WHO 404-temperaturenot looked up); the vanished-survivor guard.binary_sensor.async_setup_entrywith{mac}-04-motionlisted before{mac}-1-0004-motionin the registry (real registries,where: "0004"in the configured platform): oneMyHOMEMotionSensorwith unique id{mac}-1-04-motion,binary_sensor.hall_motionkept 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-motionsharing the device{mac}-1-04with 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_entrywith{mac}-04-illuminancelisted 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).0015and21untouched, the0005/05collision resolved for05, second run writes nothing) and the read fallback.…-1-0004with a bus light…-1-04and its two buttons: after the migration the cover is…-1-04on 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-15light next to a switch (not an area-0 point, untouched).{mac}-04-motionrestored andbinary_sensor.hall_motiongone; the other four with the customisations not carried). Onv2-phase1-architecture(d8bac15) the module does not import (canonical_point_whereis new); with that import guarded, 9 of the original 22 fail.mastercheckout (f04d8aa) onPYTHONPATH, 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.pypass. Fixture MAC00:03:50:00:01:47, no hosts.Where are the traces
tests/fixtures/traces/f461_live_2026-10-23/…json,run2_reviewer_readsandrun1_oracle_replay_readonly;tests/test_f461_live_2026_10_23_trace.py::test_area0_point_spellings_0007_nack_07_ack_7_is_area_7WHERE 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 as07and never as0007.tests/fixtures/traces/myhomeserver1_live_2026-10-24/…json,run3_reads_lights_addresses_humidityandrun2;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*#13**16*3*87*13##, WHO 1013 model67), 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-0015must stay as typed and only000Nis canonicalised, which is what the validator does. Note: the oracle emulates MyHomeServer1 2.82.06; this unit is newer.tests/fixtures/traces/f461_live_2026-10-15/myhome_trace_F461_live_2026-10-15.json, runprobe_area0(rows, command session)*#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.tests/fixtures/traces/issue_445/myhome_trace_MyHomeServer1_LN4660M2_centralized_down_then_stop_2026-09-26.json*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.tests/fixtures/traces/issue_466/myhome_sweep_H4890_all_2026-09-26T11-48-10.json*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##(never0005/0006/0008).tests/fixtures/traces/issue_619/myhome_sweep_F453AV_all_2026-10-04T19-45-11.json*2*0*05##,*2*0*06##in the*#2*0##sweep reply, next to*2*0*69##,*2*0*74##… (never0005/0006).tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json,run1_oracle_replay_readonlystepplant_reads(poll_cover_05),run4_final_state,run4b_final_state_after_restoreWHERE 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 as05. 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:*#1*0001##,*#1*0002##,*1*1*0001##*1*1*0001##→ bus$0301001200, own*1*1000#1*01##)*#1*02##$0302001500/$00302001500)So the 2-digit form is the one the gateways put on the bus, and the 4-digit
000Nform is refused everywhere except on the MH202. No oracle stimulus exists for the registry migration (it touches no frame).lights-area0is a WHO 1 suite only (its stimuli are*#1*…##/*1*…##:0,00,0001,0002,0002#4#01,0010,0010#4#01,0015,02,2and the interface variants). 4-digit WHO 2 polls and commands (*#2*000N##,*2*1*000N##) have not been replayed in the oracle: noresults/**/*.tsvat 52d354b holds such a stimulus. The WHO 2 side of this PR rests on the sharedA/PLgrammar (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 SCSA/PLpoint-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 sharedA/PLgrammar, not by a separate WHO 2 oracle stimulus, which is why the cover andmembersWHERE take the same rewrite.ownkb:chunk:r000604/ownkb:claim:c003426: WHO 5 (burglar alarm) addressing is an independent grammar (zones, sensors, central unit), not theA/PLfamily, which is whyalarm_control_panel_schemapassesPointToPoint(canonical=False)and0004stays as typed there.Review follow-up (review 5639727669)
migrate.py)refactor(where_grammar)myhome.yamlcopy (migrated_yaml_platforms) canonicalised on read and write, churn scenario testedfix(legacy_yaml)expose(normalize_expose_where,_validate_expose_entry,config_flow_expose.py) and theexposesubentriesfix(expose)binary_sensor/sensorschemas and thebinary_sensor/sensordomains in the migrationfix(validate)cover_travel_timesre-keyedfix(migrate)labelson the device merge;area_id/labels/iconfrom the removed duplicate; release note on theA/PLattributesfix(migrate)#799 (merged), base SHA d8bac15, solution item 1, Encyclopedia provenanceThe first commit (
fix(validate)) is unchanged.Review follow-up (review 5642942172)
Rebased on
v2-phase1-architecture089fef5 (the seven PR commits unchanged in content; shas moved).[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 throughbinary_sensor/sensorasync_setup_entrywith the twin listed first, including the order that removed the user's entity -> 5fb1f18fix(migrate). Your reading ofbuild_motion/MyHOMEMotionSensorandbuild_illuminance/MyHOMEIlluminanceSensoris confirmed. One precision on the two consequences: in the straightforward boot (yaml{mac}-1-0004-motionon{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.[P2]ruff 0.16.7I001on the inline import in_remove_ghost_light(ruff 0.17.2 did not flag it): the rationale comment moved above the import -> bd9de12style(migrate);ruff check custom_components tests scriptspasses with 0.16.7 and 0.17.2,scripts/verify_ha_standards.pypasses.[P2]"Where are the traces": the un-ingested combined re-test row moved to Hardware Verification; thef461_live_2026-10-15probe_area0command 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 thatlights-area0is WHO 1 only and that 4-digit WHO 2 polls / commands have not been replayed -> this body.[P3]#135dropped from the release note, the PR title and thevalidate.py/migrate.pydocstrings (the test module docstring too); the release note and the validator docstring say the0809of 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 -> f764154docs(validate).Checklist
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).f461_live_2026-10-15,issue_445,issue_466,issue_619andf461_live_2026-10-23traces, and the publishedlights-area0oracle rows above.pytest,ruff, andscripts/verify_ha_standards.pypass.🤖 Generated with Claude Code