Device registry: scope every lookup to its entry, and resolve panel ids from before 2026.8 - #270
Merged
Merged
Conversation
Home Assistant 2026.8 gave every device a single owning config entry. It reports `async_get_device(identifiers=...)` as deprecated and says it stops working in 2027.8: it searches every config entry, but an identifier is unique only within one. The panel's own registration had already moved to `async_get_device_by_identifier`; four lookups had not -- the extension card check, the adopted identity freeze, and both card lookups the curation editor makes. The ambiguity is latent rather than live. A second SPAN entry cannot hold this panel's identifiers today, because the config flow keys entries on the serial every identifier embeds. Were it reached, an adopted device would be frozen onto the other entry's identifier -- permanently, since the freeze is by design irreversible -- the editor would group rows under that entry's device, and an extension row would be admitted around the deferral and mint a nameless card of its own. That guard is a property of the flow rather than of the registry, and the lookups no longer rest on it. Each lookup now takes the entry it is acting for. The scope threads through the extension partition, the adopted identity resolution, the curation rows, the seven entity builders and the five platform setups, which state it beside the panel link they already pass. A wrong scope fails silently -- an adopted entity re-minted under a new unique_id, which the registry reads as a replacement that takes its history, or an extension entity that never appears -- so every site is pinned by a test that registers the cards first: the freeze through the builders, and each platform's setup in test_platform_entry_scope.py. Replacing the scope with a bogus entry at any of the eight sites is caught by the tests aimed at it. The identity-freeze tests that stubbed the registry with a lambda, blind to the scope, now run against the real registry, and three tests cover the other-entry case directly. The two WebSocket commands that take a panel device id found its entry by looping over `DeviceEntry.config_entries`, which 2026.8 reduced to a deprecated shim holding the device's one owner. They now share `resolve_panel_device`, which checks the owning entry's domain. The two copies had drifted: topology took an arbitrary member of the set without checking its domain, so it refused a SPAN identifier on another entry's device as not loaded where the adopted commands answered not a panel. A device id saved before 2026.8 is resolved to the device it became. That release split every device with several config entries into one device per entry, each with a new id, and a SPAN panel was one whenever a helper built on its sensors had linked itself to the panel's device; the dashboard card keeps the id it was configured with. The registry answers such an id with a read-only composite until 2027.8, but its `config_entry_id` is the old device's primary and no sub-device points at it any more, so topology returned no sub-devices -- or, depending on the hash seed, refused a loaded panel as not loaded. The resolver maps the old id to the split a SPAN entry owns through `async_get_devices_for_composite_device_id`, and topology matches sub-devices against that device while still echoing the id it was sent. A child device, which `async_get` can answer with from 2026.9, is refused as not found, as the helper 2026.9 adds for this lookup refuses it; a comment marks where that helper replaces the lookup once 2026.9 is the minimum. The tests reach that registry through Home Assistant's own migration of a pre-2026.8 store, with either entry as the old device's primary. Topology also names the panel's current device and the entry that owns it, `panel_device_id` and `config_entry_id`. The card looked the id it holds up in the device list, which has no entry for an old id, so it lost the panel's config entry and the area its circuits fall back to. The card, rebuilt from span-card 849aefc, now takes both from topology, and it and the dashboard stop reading the device list's deprecated `config_entries`. The tests that still looked devices up with the unscoped call now ask within their entry. websocket-api.md records that an old id is still accepted, developer.md names the shared resolver, and the 2.1.2 changelog describes both fixes as a user meets them.
Rebuilt from span-card main at 1c65fd3, which merges the empty sub-device skip (#15) beside the topology identity fix (#16) this bundle already carried. A sub-device with nothing to draw -- the Microgrid Interconnect, whose one entity is a diagnostic -- is left off the card rather than drawn as a bare header and settings icon.
Home Assistant 2026.9 lets `DeviceRegistry.async_get` answer with a child device, a part of another device that has no `via_device_id`. Two places read that field off whatever `async_get` returned: the favorite service, walking an entity's device up to its panel, and the new-entity announcement, naming the sub-device an entity sits on. mypy against 2026.9 flags both, and at runtime a child device answers the missing field with a deprecation warning. Neither can concern SPAN: the integration registers no child devices, and a `via_device_id` can never name one. So a child device is treated as the absence of a SPAN device, with the `isinstance` narrowing `resolve_panel_device` uses, which holds on 2026.8 and 2026.9 alike. The favorites service tests faked devices with bare mocks and replaced the whole `dr` module, so no fake was a `DeviceEntry`; they are now spec'd to it, with the real class kept on the patched module, and a new test refuses an entity on a child device -- it fails if the check goes back to testing for None. The adoption test that listed devices through `registry.devices` as a mapping, which 2026.9 deprecates and raises on in tests, lists this entry's devices through `async_entries_for_config_entry`.
Brings in the ha-core bump (#267), which moved the pins to Home Assistant 2026.9.0 without relocking. uv.lock is relocked against them: Home Assistant, its stubs and the test plugin, and the dependency changes 2026.9.0 brings with it; no span library moves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
device_registry.async_get_deviceis resolved. Every device lookup by identifier is now scoped to the config entry it acts for, throughasync_get_device_by_identifier; the unscoped call stops working in Home Assistant 2027.8.websocket_panel.resolve_panel_device, instead of two copies that had drifted — topology took an arbitrary one of a device's entries where the adopted commands checked its domain. Neither reads the deprecatedDeviceEntry.config_entriesany more.not_loaded.panel_device_idandconfig_entry_id. The card uses them (Find the panel by the device topology names span-card#16), so a card holding an old id works with nothing to change. The bundle is rebuilt from span-cardmainat 1c65fd3, which also brings the empty sub-device skip (Skip a sub-device with nothing to draw span-card#15).How
config_entry_idis threaded through the extension partition, the adopted identity resolution, the curation rows, the seven entity builders and the five platform setups.resolve_panel_devicerefuses a child-device id as not found, and a comment marks where Home Assistant 2026.9'sasync_get_device_and_config_entry_for_domainreplaces its lookup once 2026.9 is the minimum.CI
mainmoved to Home Assistant 2026.9.0 (#267, merged withlint-and-testfailing on eight mypy errors, all because 2026.9 letsasync_getanswer with a child device, which has novia_device_id). This PR fixes all eight. Four were in the WebSocket handlers it rewrites; inservices.py(the favorite service) andadditions.py(the new-entity announcement), a child device is now treated as the absence of a SPAN device, since SPAN registers none — the sameisinstancenarrowingresolve_panel_deviceuses, which holds on 2026.8 and 2026.9 alike. A test that read the device registry as a mapping, which 2026.9 deprecates, now lists the entry's devices instead.The branch merges
mainand relocksuv.lock, which #267 left at 2026.8.3. CI is green on 2026.9.0, and the suite also passes on 2026.8.3, the minimum supported version.