Skip to content

ebus_default_override's battery entry cannot match a conformant device #27

Description

@cayossarian

Found while adopting .ebus-spec.json for our own publisher, so the provenance convention
earned its keep on the first outing.

The bug

_capability_of derives the lookup key from the node $type:

marker = ".capability."
if marker in node_type:
    return node_type.rsplit(marker, 1)[-1]
return ctx.node_id or None          # fallback: node id

The _EBUS_DEFAULTS table is keyed "meter", "battery", "info". Two of those are
capability names. battery is not — the specification has no battery capability. State
of charge lives in capabilities/soc.json (energy.ebus.capability.soc, v0.1), whose
properties are soc, soe, total-energy-storage, loadup-headroom.

So for a device publishing a node typed energy.ebus.capability.soc, the key is soc, the
lookup misses, and the entry never fires. It is reachable only through the node-id fallback —
a node whose $type is absent or non-eBus and whose id is literally battery.

The reference publisher does not produce that. A BESS profile publishes:

"soc": { "type": "energy.ebus.capability.soc",
         "properties": { "soc": {"name": "State of charge"},
                         "soe": {"name": "State of energy", "unit": "kWh"} } }

The consequence is exactly what the entry's own comment says it exists to prevent:

# A bare percent is ambiguous to inference; here it is unmistakably SoC.
"soc": {"device_class": "battery", "state_class": "measurement"},

soc is a bare %, generic inference deliberately leaves a bare % unresolved, and the
override that would resolve it does not run. A battery SoC reaches Home Assistant with no
device_class.

The fix

Rename the key batterysoc. One line.

Worth checking the property set in the same pass, since it does not match the catalog either:
the table carries soc, state-of-charge, power, temperature, while soc v0.1 defines
soc, soe, total-energy-storage, loadup-headroom. soe is a kWh energy figure with no
entry; state-of-charge, power and temperature are not soc properties. Whether those
belong under other capabilities is your call — we did not want to guess.

The general shape, since this is the interesting part

This is table-versus-catalog drift, and the SDK's lockfile shows the same picture
independently. Running your own tool:

$ python3 tools/drift-report.py --manifest spec-manifest.json ../python-sdk/.ebus-spec.json
python-sdk/.ebus-spec.json  [role=library, synced=9be790ba 2026-07-11]
    BEHIND  framework: pinned 0.5, current 0.7
    BEHIND  capabilities/info: pinned 0.1, current 0.2
    BEHIND  capabilities/meter: pinned 0.1, current 0.2
    BEHIND  devices/utility-meter: pinned 0.3, current 0.6
    BEHIND  registries/capability-types: pinned 0.11, current 0.19

(The two framework features not yet supported lines the tool also emits are the deliberate
exclusions your notes already document — broker-hosting and rest-configuration — so
they are not findings.)

customize.py encodes capability facts but is not covered by any of those pins — the
implements map reflects examples/utility-meter, as your notes say, and soc does not
appear in it at all. So nothing in CI compares that table against the catalogs it restates,
which is why a key can stop matching without anything failing.

Happy to do any of this

We are adopting the provenance convention for our own simulator this week, so the technique is
loaded either way:

  • The one-line fix plus a regression test that drives a node typed
    energy.ebus.capability.soc through the override and asserts device_class: battery comes
    out. Small, and it pins the behaviour rather than the spelling.
  • The lockfile re-sync — bump the pins above, refresh synced_commit. Mechanical.
  • A content check. panel-sim's test_catalog_drift.py already does the harder half of
    this (lockfile-vs-catalog versions always, vendored-vs-source content when a
    ../specification checkout is present). The same pattern applied to _EBUS_DEFAULTS
    assert every key names a real capability and every property exists in that capability's
    catalog — would have caught this at authoring time, and would keep catching it. It works
    regardless of how you decide the wider question of where capability data should live.

Say which, or none. We would rather offer than just report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions