Skip to content

fix(emitter): derive circuit ids from panel serial to match panelbench - #50

Merged
cayossarian merged 1 commit into
mainfrom
fix/panel-scoped-circuit-ids
Aug 29, 2026
Merged

fix(emitter): derive circuit ids from panel serial to match panelbench#50
cayossarian merged 1 commit into
mainfrom
fix/panel-scoped-circuit-ids

Conversation

@cayossarian

@cayossarian cayossarian commented Aug 29, 2026

Copy link
Copy Markdown
Member

Why

stable_circuit_uuid derived a circuit's uuid from the YAML circuit id alone. That id is a key every panel configuration reuses — solar_inverter, oven — so it was not unique to a panel.

PanelBench publishes circuits flat, at ebus/5/<circuit-uuid>/… with no serial in the path, so an unscoped id collided there on the wire. It now scopes by the panel serial (panelbench#22, released as 2.5.3).

This repository must derive the same id byte for byte. Stopping this simulator and starting PanelBench is how a firmware upgrade is rehearsed on a single panel — the scenario 1.2.1 exists to make survivable — and Home Assistant keys a circuit's entities on the id the panel publishes. While the two repositories derived that id differently, every circuit changed identity at the swap and its history and automations were stranded on the old device.

Scope note

Scoping is also correct here in its own right: a circuit id shared across panels is one identity to any consumer keyed on the node id alone.

But it was not a collision on this simulator's own wire, and this PR does not claim to fix one. wire/mapping/circuit.yaml places a circuit node-on-parent with device_id_source: parent and $description_owner: parent, so a circuit is a node on the panel device rather than a device of its own. Verified against the running emitter: of the 66 topics a panel publishes, all 66 are rooted at ebus/5/<serial>/, and the uuid never appears at a topic level without the serial above it. Two simulated panels never overwrote each other's retained values.

The change

stable_circuit_uuid(panel_id, circuit_id) hashes f"{panel_id}/{circuit_id}" under the unchanged namespace.

The serial is panel_config.serial_number, derived once per module and threaded down rather than re-read by each helper. A second derivation would be free to read a different key (a file name, a container hostname, a CLI override read before the sim- prefix is applied); that would break the contract without colliding with anything. engine.serial_number is that same key read after the prefix is applied, which is why runtime and engine use it.

Callers threaded: spec_generator (circuit instances, PV feed, EVSE feeds), runtime (uuid_to_circuit_id, the EVSE tick inputs and their feed lookup), engine.get_tick_inputs.

Cross-repo contract

tests/test_instance_ids_contract.py pins the literal:

stable_circuit_uuid("sim-40t-001", "solar_inverter") == "be87c32bda4f5cd9abbf6d3995ae28c0"

Confirmed identical with the PanelBench owner, which pins the same literal — so a one-sided change to the derivation fails both suites.

Upgrade note

Circuit ids change, so a simulated panel already added to Home Assistant has to be removed and re-added — its circuit entities' unique ids move with the ids. Nothing else about the panel changes, and a panel added after this release needs nothing.

Verification

  • pytest tests/ -q → 494 passed, 3 failed. The three failures are pre-existing and environmental (the installed ebus-sdk in this venv takes Device(parent=…) where graph_builder passes parent_id=, and no longer sets the homie logger to INFO); they reproduce identically on an unmodified tree and touch none of this change.
  • pre-commit run --all-files → all hooks pass, mypy included.
  • Version 1.2.1 → 1.2.2 across all four strings via scripts/sync-version.sh.

@cayossarian
cayossarian force-pushed the fix/panel-scoped-circuit-ids branch from 590558b to c515c8e Compare August 29, 2026 02:33
`stable_circuit_uuid` derived a circuit's uuid from the YAML circuit id
alone. That id is a key every panel configuration reuses (`solar_inverter`,
`oven`), so it was not unique to a panel.

PanelBench publishes circuits flat, at `ebus/5/<circuit-uuid>/...` with no
serial in the path, so an unscoped id collided there on the wire; it now
scopes by the panel serial. This repository must derive the same id byte for
byte. Stopping this simulator and starting PanelBench is how a firmware
upgrade is rehearsed on a single panel, and Home Assistant keys a circuit's
entities on the id the panel publishes -- so while the two derived that id
differently, every circuit changed identity at the swap and its history and
automations were stranded on the old device.

The derivation now hashes `<panel-serial>/<circuit-id>`, and
`tests/test_instance_ids_contract.py` pins the literal PanelBench pins, so a
one-sided change fails both suites.

Scoping is also correct here in its own right -- a circuit id shared across
panels is one identity to any consumer keyed on the node id alone -- but it
was not a collision on this simulator's own wire: `wire/mapping/circuit.yaml`
places a circuit `node-on-parent` with `device_id_source: parent`, so every
topic is rooted at the panel serial and two simulated panels never overwrote
each other's retained values.

The serial is `panel_config.serial_number`, read once per module and threaded
down rather than re-derived: a second derivation would be free to read a
different key and would break the contract without colliding with anything.

Circuit ids change, so a simulated panel already added to Home Assistant has
to be removed and re-added.
@cayossarian
cayossarian force-pushed the fix/panel-scoped-circuit-ids branch from f117362 to 4299fe7 Compare August 29, 2026 02:38
@cayossarian cayossarian changed the title fix(emitter): scope circuit device ids to the owning panel fix(emitter): derive circuit ids from panel serial to match panelbench Aug 29, 2026
@cayossarian
cayossarian merged commit 1645566 into main Aug 29, 2026
2 checks passed
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.

1 participant