Adopted-entity curation: user-asserted metadata from a new Adopted tab - #266
Merged
Conversation
…toring an unreadable one
Read the stored overlay into runtime data before the platforms are forwarded, so every adopted entity is born with its curated metadata rather than having it applied after its first state is written. The field is required rather than defaulted: a setup path that forgets the load has to fail loudly, because an empty overlay is indistinguishable from a user who has curated nothing. Removing the entry forgets the store, as it already forgets the announcement record. The keys are wire addresses rather than registry ids, so a store left behind is one the next entry for the same panel would load and apply.
developer.md gains a Curation section covering the .storage overlay and its scope-prefixed keys, validation refusing at save and dropping at construction, the description helpers that keep both AST guards absolute, the two websocket commands' zero-registry-writes boundary, and why the reload is the mechanism rather than a courtesy. The revisable-vs-not table's state_class row moves from "never set at all" to user-curated only, and the diagnostics section gains the adopted_curation block's withholding rule. README.md documents the Adopted tab: what the integration sets versus what is Home Assistant's own, that saving reloads, the total_increasing and statistics-class-removal consequences, and that identity never changes.
…tion # Conflicts: # CHANGELOG.md
…e classes match it A unit-less reading was published as text however the wire declared it: the unit stood in for "this is numeric", so a bare count arrived as the string "42". That was harmless while an uncurated row asserted nothing about itself, and stopped being harmless once its owner could put a `measurement` on exactly that row -- the recorder would be handed a string under a numeric state class. `declares_a_number` now answers the question once, in `util`, for the union of a declared unit and a numeric `$datatype`, so nothing that parses today stops parsing and a bare count parses too. The device-class offer was gated on the declared unit alone, which let a text row be offered `power_factor`, `aqi` and `monetary` -- classes constraining no unit, and so passing vacuously -- each of which reads unknown for the life of the install. Core's own `NON_NUMERIC_DEVICE_CLASSES` partitions the vocabulary and `declares_a_number` says which side a row falls on, so the editor offers one half or the other and the validator refuses the crossing with `incompatible_device_class`. Sharing the predicate with the parse is what keeps a row read as a float from being offered nothing but `enum`.
The Adopted tab no longer offers an icon: it wrote through the same registry command Core's own entity settings dialog issues, so it was a second place to set one thing. The field lists in the README and frontend.md drop it and say where the icon and the area are set instead. developer.md's validation section gains the datatype half of the device-class gate, and the reason `declares_a_number` is one predicate rather than two: the same answer decides whether a reading is parsed as a number and which half of Core's device-class vocabulary the row is offered.
The condensed Adopted sections left an orphaned </details>, a bullet with scrambled markup, two sentences split mid-phrase, and two dead cross-references (#adopted-vendor-readings, and #bess--grid-management which bess-grid-management.md links to). Restore the BESS heading level so its four subsections are parented again, and give "the confirmation" and the reading-vs-control distinction the antecedents their cut paragraphs used to supply. Normalize documentation prose to US spelling. The identifier references `is_modelled` and the normalising test name stay en-GB so they still name the Python symbols they refer to.
…eases The lockfile still recorded homeassistant 2026.8.0 and span-panel-api 3.3.0 while pyproject pins 2026.8.3 and 3.4.1, so uv lock --check failed and every bare uv run re-resolved and left the file dirty.
The span-panel-api path in [tool.uv.sources] is relative to the repository root, so it arrives from the primary checkout and from a worktree beside it, and misses from one nested inside the checkout at .claude/worktrees/<name>, which sits two levels deeper. uv run then cannot build an environment at all, so pylint, mypy, vulture, both radon hooks and the test suite fail before they start -- and the first commit from a new worktree is where you find out. setup-hooks.sh now bridges the gap with a symlink, both ends derived from git rather than written down, and asks git for the pre-commit hook path rather than assuming .git/hooks, which a worktree's .git file is not. check-library-path.py claimed the relative path resolves to the same directory from every worktree. It does not, and the claim is why this reads as a broken pin rather than a path-depth problem.
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
Adopted entities (adopted devices and vendor readings on modelled devices) can now be curated by the user: a device class, a statistics class, and prominence are asserted from the dashboard's new admin-only Adopted tab, stored in a per-entry
.storageoverlay keyed by scope-prefixed wire addresses, and applied when entities are constructed — so after the automatic reload a curated entity is born with its metadata rather than patched afterwards.How
curation.pyowns the record type, validation (refuses at save, re-runs at construction — stale fields drop with one warning and never block setup), the store, and the description helpers. It is the single audited module that may spellstate_class; the AST guards onadoption.pyandextension.pystay absolute, andextension.pygains the guard it was missing.span_panel/adopted/list(rows grouped by device, with server-computed allowed classes and stale-field marks) andspan_panel/adopted/curate(validate → save → schedule reload — zero registry writes, asserted by object identity in tests).disabled_byis omitted whenever the enable control is untouched, so an integration-disabled entity is never silently re-attributed to the user.adopted_curationblock (keys and enum values only). Docs updated: README, developer.md, websocket-api.md (including the pre-existingnot_panel_devicedrift), frontend.md, CHANGELOG.chore(release): 2.1.1b3.Testing
Full suite on the merged tree: 1765 passed, 1 skipped (89 new tests across curation, adoption, extension, websocket, diagnostics). mypy/ruff clean; typing and entity-id audits pass.