Code references verified against v0.5.0 and main. Spec references are to the commit this repo pins in .ebus-spec.json (4254526), so they are the versions already synced here.
What the spec models
devices/distribution-enclosure.md 0.12 treats loads beyond the feedthrough as belonging to another enclosure, not to this one:
The upstream feed may come from the utility service (the common case), from an upstream BESS wired between the utility and the enclosure, or from another (parent) distribution enclosure in a multi-enclosure install. The downstream side may similarly feed loads directly, feed a sub-enclosure via feedthrough lugs, or both.
and on the lugs device itself:
Each enclosure has upstream lugs (grid connection) and, when the enclosure supports it, downstream lugs (feedthrough for a sub-enclosure or for chaining enclosures).
The wiring relationship between a lugs device and what is wired to it is expressed as a structured feeds-device-id / fed-by-device-id reference on connection.
devices/circuit.md 0.3 rules out the alternative reading, in which circuits hang off a lugs device:
A circuit is a Homie child device of some parent, but the parent may be any device: a distribution enclosure (branch circuits), a proxy (a proxied smart breaker or an eGauge/EKM meter-point), or a sub-enclosure.
and, on the circuit device itself:
A circuit represents one conductor path. It has no eBus-modelled child devices.
and energy.ebus.device.lugs is itself a circuit specialization ("a circuit plus a direction"), so a lugs device can never be a circuit's parent. Circuits beyond the feedthrough are children of the sub-enclosure, and the two enclosures are joined by connection records.
Multi-enclosure composition is a modelled case, not an extrapolation. Example 1 of the enclosure model is a three-panel SPAN chain, and its answer to how the panels form one system is:
Each panel is an independent Homie root device on its own MQTT broker. Each broker reports only what its panel knows. The data model accommodates the per-panel views — the wiring topology is the union of connection records across the three brokers.
That example also shows this exact gap in the field, on panel 1's lugs-dn:
(no connection/feeds-* — panel 1 does not currently know it feeds panel 2; future commissioning may populate this side of the inter-panel feedthrough)
What this package models instead
manifest_physics.py:402 requires a per-circuit placement of upstream-of-lugs or downstream-of-lugs; panel_meter.py defines feedthrough as the sum over the downstream-of-lugs circuits, with the downstream lugs' currents and energies drawn from the same subset.
Those circuits are nonetheless published as children of this enclosure — wire/mapping/circuit.yaml is unconditional:
placement:
kind: child-of-parent
parent_entity_class: panel
So a circuit marked downstream-of-lugs is emitted as this enclosure's child while its power is counted as leaving through the feedthrough to another one. The discriminator is also invisible on the wire: wire/profiles/circuit.json selects only feeds-device-id, feeds-device-type, feeds-device-status and count on connection, with no fed-by-*, so a circuit cannot state what feeds it. A consumer sees N circuits belonging to this enclosure and an aggregate asserting they are pass-through, with nothing published to reconcile the two.
examples/forty_tab_minimal.yaml sets placement on none of its five circuits, and examples/run_forty_tab_minimal.py defaults it:
"placement": str(circuit.get("placement", "downstream-of-lugs")),
so the reference run applies this to every circuit. A visible consequence is that the two lugs devices then publish byte-identical active-power, current-a, current-b, imported-energy and exported-energy: the downstream subset equals the whole, so upstream and downstream coincide exactly.
We inherited that default line into our own generator and spent a while writing the identical values up as a fidelity gap in the lugs model before finding it was a config answering a question the library had deliberately left open with _require.
What cannot currently be published
wire/profiles/lugs.json selects seven connection properties. Measured on the retained tree of a 40-tab run:
| device |
declared |
published |
lugs-upstream |
7 |
3 — the fed-by-* trio |
lugs-downstream |
7 |
0 |
feeds-device-id, feeds-device-type, feeds-device-status and count are never published by either device. Every layer exists except the assignment:
| layer |
state |
snapshot.py:134-137 |
EbusLugsSnapshot carries all four fields |
wire/bag_builder.py:311-314 |
resolvers bind all four to connection/* |
emitter.py:760-772 |
constructs the snapshot and passes none of them |
manifest_physics.py:354-360 |
_parse_lugs reads direction only, so no manifest key could supply them |
The same three are populated one device class over, from a graph edge (emitter.py:709-711, circuit snapshots), so the pattern exists and works. feeds-role — the catalog's alternative for a downstream node that is not its own eBus device, whose values include SUBPANEL — is not selected in the lugs profile either, so neither branch of the catalog's dichotomy is currently expressible.
The upstream side is populated but only one hop deep: upstream_fed_by (emitter.py:662-667) is set from an in-manifest BESS with relative-position: UPSTREAM, so nothing can say what is upstream of the BESS — the utility service, or the parent enclosure the model names.
The consumer symptom is the one #26 closed for info/model: a property declared in $description and never published becomes an entity that is created, waits for a value, and never updates. A catalog-conformance check cannot see it, since declared-and-never-published is conformant by construction; it surfaces only against a capture carrying values.
The ask
Model the devices, and the telemetry follows rather than the other way round. If the manifest can express what sits on each side of a lugs device — the references the enclosure model designates for it — then loads beyond the feedthrough belong to a sub-enclosure, feedthrough derives from the device graph, and the per-circuit placement flag is no longer needed to approximate it. Circuit powers already arrive through TickInputs.circuits, so nothing new per-tick is implied by that shape.
Deliberately not proposing how a sub-enclosure should be realised — a second enclosure within one manifest, or a separate emitter — since that is a design call with consequences not visible from outside. Tying several emitters into a coherent system is not being asked for either: the spec's own answer is the union of connection records across independent roots, which is an application concern and one we are treating as ours. Emitter is one per logical tree and publish_tick is caller-driven, so running several against one broker already works and needs nothing from this package. What cannot be done from outside is state the relationship on the wire.
Mentioned only as timing: python-sdk#47 is reshaping graph_builder now, and a property whose value names another device is a graph edge — the same point as python-sdk#49.
Cheap mitigation, independent of the above
Marking most circuits upstream-of-lugs in examples/forty_tab_minimal.yaml would stop the reference run emitting an indistinguishable lugs pair, and is a more faithful picture of a main panel besides. A default in an example silently defeats a _require in the library for everyone who copies the example, which is the #49 pattern again: the rule lives in a script, gets hand-copied, and goes wrong quietly.
Code references verified against
v0.5.0andmain. Spec references are to the commit this repo pins in.ebus-spec.json(4254526), so they are the versions already synced here.What the spec models
devices/distribution-enclosure.md0.12 treats loads beyond the feedthrough as belonging to another enclosure, not to this one:and on the lugs device itself:
devices/circuit.md0.3 rules out the alternative reading, in which circuits hang off a lugs device:and, on the circuit device itself:
and
energy.ebus.device.lugsis itself a circuit specialization ("a circuit plus adirection"), so a lugs device can never be a circuit's parent. Circuits beyond the feedthrough are children of the sub-enclosure, and the two enclosures are joined byconnectionrecords.Multi-enclosure composition is a modelled case, not an extrapolation. Example 1 of the enclosure model is a three-panel SPAN chain, and its answer to how the panels form one system is:
That example also shows this exact gap in the field, on panel 1's
lugs-dn:What this package models instead
manifest_physics.py:402requires a per-circuitplacementofupstream-of-lugsordownstream-of-lugs;panel_meter.pydefines feedthrough as the sum over thedownstream-of-lugscircuits, with the downstream lugs' currents and energies drawn from the same subset.Those circuits are nonetheless published as children of this enclosure —
wire/mapping/circuit.yamlis unconditional:So a circuit marked
downstream-of-lugsis emitted as this enclosure's child while its power is counted as leaving through the feedthrough to another one. The discriminator is also invisible on the wire:wire/profiles/circuit.jsonselects onlyfeeds-device-id,feeds-device-type,feeds-device-statusandcountonconnection, with nofed-by-*, so a circuit cannot state what feeds it. A consumer sees N circuits belonging to this enclosure and an aggregate asserting they are pass-through, with nothing published to reconcile the two.examples/forty_tab_minimal.yamlsetsplacementon none of its five circuits, andexamples/run_forty_tab_minimal.pydefaults it:so the reference run applies this to every circuit. A visible consequence is that the two lugs devices then publish byte-identical
active-power,current-a,current-b,imported-energyandexported-energy: the downstream subset equals the whole, so upstream and downstream coincide exactly.We inherited that default line into our own generator and spent a while writing the identical values up as a fidelity gap in the lugs model before finding it was a config answering a question the library had deliberately left open with
_require.What cannot currently be published
wire/profiles/lugs.jsonselects sevenconnectionproperties. Measured on the retained tree of a 40-tab run:lugs-upstreamfed-by-*triolugs-downstreamfeeds-device-id,feeds-device-type,feeds-device-statusandcountare never published by either device. Every layer exists except the assignment:snapshot.py:134-137EbusLugsSnapshotcarries all four fieldswire/bag_builder.py:311-314connection/*emitter.py:760-772manifest_physics.py:354-360_parse_lugsreadsdirectiononly, so no manifest key could supply themThe same three are populated one device class over, from a graph edge (
emitter.py:709-711, circuit snapshots), so the pattern exists and works.feeds-role— the catalog's alternative for a downstream node that is not its own eBus device, whose values includeSUBPANEL— is not selected in the lugs profile either, so neither branch of the catalog's dichotomy is currently expressible.The upstream side is populated but only one hop deep:
upstream_fed_by(emitter.py:662-667) is set from an in-manifest BESS withrelative-position: UPSTREAM, so nothing can say what is upstream of the BESS — the utility service, or the parent enclosure the model names.The consumer symptom is the one #26 closed for
info/model: a property declared in$descriptionand never published becomes an entity that is created, waits for a value, and never updates. A catalog-conformance check cannot see it, since declared-and-never-published is conformant by construction; it surfaces only against a capture carrying values.The ask
Model the devices, and the telemetry follows rather than the other way round. If the manifest can express what sits on each side of a lugs device — the references the enclosure model designates for it — then loads beyond the feedthrough belong to a sub-enclosure, feedthrough derives from the device graph, and the per-circuit
placementflag is no longer needed to approximate it. Circuit powers already arrive throughTickInputs.circuits, so nothing new per-tick is implied by that shape.Deliberately not proposing how a sub-enclosure should be realised — a second enclosure within one manifest, or a separate emitter — since that is a design call with consequences not visible from outside. Tying several emitters into a coherent system is not being asked for either: the spec's own answer is the union of
connectionrecords across independent roots, which is an application concern and one we are treating as ours.Emitteris one per logical tree andpublish_tickis caller-driven, so running several against one broker already works and needs nothing from this package. What cannot be done from outside is state the relationship on the wire.Mentioned only as timing:
python-sdk#47is reshapinggraph_buildernow, and a property whose value names another device is a graph edge — the same point aspython-sdk#49.Cheap mitigation, independent of the above
Marking most circuits
upstream-of-lugsinexamples/forty_tab_minimal.yamlwould stop the reference run emitting an indistinguishable lugs pair, and is a more faithful picture of a main panel besides. A default in an example silently defeats a_requirein the library for everyone who copies the example, which is the#49pattern again: the rule lives in a script, gets hand-copied, and goes wrong quietly.