fix(emitter): derive circuit ids from panel serial to match panelbench - #50
Merged
Conversation
cayossarian
force-pushed
the
fix/panel-scoped-circuit-ids
branch
from
August 29, 2026 02:33
590558b to
c515c8e
Compare
`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
force-pushed
the
fix/panel-scoped-circuit-ids
branch
from
August 29, 2026 02:38
f117362 to
4299fe7
Compare
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.
Why
stable_circuit_uuidderived a circuit's uuid from the YAML circuitidalone. 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.yamlplaces a circuitnode-on-parentwithdevice_id_source: parentand$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 atebus/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)hashesf"{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 thesim-prefix is applied); that would break the contract without colliding with anything.engine.serial_numberis that same key read after the prefix is applied, which is whyruntimeandengineuse 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.pypins the literal: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 installedebus-sdkin this venv takesDevice(parent=…)wheregraph_builderpassesparent_id=, and no longer sets thehomielogger 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.scripts/sync-version.sh.