Skip to content

Circuit placement models a sub-enclosure as this enclosure's own circuits, and the connection properties the spec designates for it cannot be populated #30

Description

@cayossarian

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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