Skip to content

ha/customize: key the state-of-charge entry on the real soc capability - #28

Merged
dcj merged 2 commits into
mainfrom
ha-customizer-soc-capability
Aug 5, 2026
Merged

dcj merged 2 commits into
mainfrom
ha-customizer-soc-capability

Conversation

@dcj

@dcj dcj commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #27.

The customizer's SoC entry was keyed battery, which is not an eBus capability and never has been: capabilities/battery.json exists in no commit on any branch of the specification, and energy.ebus.capability.soc has carried that name in the capability-types registry since 2026-05-22, six weeks before this table was written. A conformant device typing its node energy.ebus.capability.soc therefore resolved to the key soc, missed the table, and reached Home Assistant with neither a device_class nor a state_class on its SoC: precisely the ambiguous bare percent the entry exists to resolve.

It stayed green because the defect was self-validating. The tests fabricated energy.ebus.capability.battery to exercise the entry and examples/ha-discovery-bridge published that node type, so the example's self-check asserted device_class == "battery" and passed.

The bigger half

soe, total-energy-storage and loadup-headroom were not merely uncovered. Inference sees kWh and stamps them energy + total_increasing, and these are reservoir levels that fall on discharge, so HA read every discharge as a meter reset and back-filled the drop as freshly consumed energy: phantom kWh in the Energy dashboard. They now carry energy_storage + measurement, HA's level counterpart. info/nameplate-capacity gets the same correction.

state-of-charge, power and temperature are dropped: none is a property of any eBus capability, a battery's electrical power is meter/active-power (already covered), and eBus does not model pack temperature.

Wider drift found in the same pass

  • meter/imported-active-energy and meter/exported-active-energy named properties the meter capability has never defined at any version. Inert (a Wh property already infers correctly) but drift of the same kind. Removed.
  • The four cumulative reactive/apparent registers carried no state_class, so HA kept no long-term statistics for them. The absent device_class there is deliberate and stays (HA has none for varh/VAh).
  • power-factor-{a,b,c} were missing while the system-level power-factor was present, despite being equally unitless and so invisible to inference.

Preventing recurrence

tests/test_catalog_drift.py walks _CAPABILITY_META against the machine-readable capabilities/*.json catalogs in a sibling specification checkout, so a name can no longer stop matching without something failing. It reads spec HEAD rather than the pinned synced_commit (the catalogs postdate that pin) and skips cleanly when no checkout is present, which is the case in CI today; EBUS_SPEC_DIR points it elsewhere.

The regression tests deliberately use a node id other than soc, since _capability_of falls back to the node id and a same-named node would pass even with $type resolution broken.

Lockfile

.ebus-spec.json was 48 commits behind: framework 0.5 -> 0.7, info and meter 0.1 -> 0.2, utility-meter 0.3 -> 0.6, capability-types 0.11 -> 0.19. supports is unchanged (no framework feature was added or removed across 0.5 -> 0.7). Two corrections beyond the numbers: grid, status, demand and power-quality are now pinned directly, the old notes rationale having lapsed when utility-meter 0.6 split them into standalone catalogs; and soc is pinned.

Compatibility note

This removes the battery key rather than aliasing it. A publisher that copied the old example's non-conformant node type should move to soc; the only known publisher of it was that example.

Verification

  • 547 tests pass; ruff check and ruff format --check clean.
  • Negative control on the new check: reintroducing both the battery key and a bogus meter id is caught.
  • The specification's drift-report.py is now down to the two documented framework exclusions.

Reported by @cayossarian, who also proposed the catalog check.

🤖 Generated with Claude Code

dcj and others added 2 commits August 5, 2026 15:17
#27)

The customizer's SoC entry was keyed `battery`, which is not an eBus
capability and never has been: `capabilities/battery.json` exists in no
commit on any branch of the specification, and `energy.ebus.capability.soc`
has carried that name in the capability-types registry since 2026-05-22,
six weeks before this table was written. State of charge lives in soc, so a
conformant device typing its node that way resolved to the key `soc`, missed
the table, and reached Home Assistant with neither a device_class nor a
state_class on its SoC: precisely the ambiguous bare percent the entry exists
to resolve.

It stayed green because the defect was self-validating. The tests fabricated
`energy.ebus.capability.battery` to exercise the entry and the example
published that node type, so the example's self-check asserted
device_class == "battery" and passed. Nothing in between touched a real
capability name.

Re-key the entry to `soc` and correct its properties to the ones the
capability defines. The bigger half of the fix is that soe,
total-energy-storage and loadup-headroom were not merely uncovered: unit
inference sees Wh/kWh and says energy + total_increasing, and these are
reservoir levels that FALL on discharge, so HA read every discharge as a
meter reset and back-filled the drop as freshly consumed energy. They now
carry energy_storage + measurement, HA's level counterpart. info's
nameplate-capacity gets the same correction, being a constant rather than a
register. state-of-charge, power and temperature are dropped: none is a
property of any eBus capability, a battery's electrical power is
meter/active-power (already covered), and eBus does not model pack
temperature.

Note this removes the `battery` key rather than aliasing it. A publisher that
copied the old example's non-conformant node type should move to soc; the
only known publisher of it was that example.

Wider table-versus-catalog drift found in the same pass: imported-active-energy
and exported-active-energy named meter properties that have never existed at
any version (inert, since a Wh property already infers correctly, but drift of
the same kind); the four cumulative reactive/apparent registers carried no
state_class at all, so HA kept no long-term statistics for them (the absent
device_class there is deliberate and stays, HA having none for varh/VAh); and
power-factor-{a,b,c} were missing while the system-level power-factor was
present, despite being equally unitless and so invisible to inference.

Add tests/test_catalog_drift.py, which walks the table against the
machine-readable catalogs in a sibling specification checkout so a name can no
longer stop matching without something failing. It reads spec HEAD rather than
the pinned synced_commit (the catalogs postdate that pin) and skips cleanly
when no checkout is present, which is the case in CI today. The regression
tests deliberately use a node id other than `soc`, since _capability_of falls
back to the node id and a same-named node would pass even with $type
resolution broken.

Re-sync .ebus-spec.json, 48 commits behind: framework 0.5 -> 0.7, info and
meter 0.1 -> 0.2, utility-meter 0.3 -> 0.6, capability-types 0.11 -> 0.19.
The supports list is unchanged (no framework feature was added or removed
across 0.5 -> 0.7). Two corrections beyond the numbers: grid, status, demand
and power-quality are now pinned directly, the old notes rationale ("covered
by pinning utility-meter") having lapsed when utility-meter 0.6 split them
into standalone catalogs; and soc is pinned, the customizer now having
first-class knowledge of it.

Reported by @cayossarian, who also proposed the catalog check.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The registry gap this audit surfaced is fixed upstream (specification
922b9f8 registers utility-meter plus the three BESS child roles the data
models already declared), so pin device-types 0.5 and re-point synced_commit
at that commit rather than shipping a lockfile that goes stale the moment the
upstream fix lands.

drift-report.py is now down to the two documented framework exclusions
(broker-hosting, rest-configuration), which are deliberate for role=library.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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.

ebus_default_override's battery entry cannot match a conformant device

1 participant