Skip to content

Releases: electrification-bus/distribution-enclosure-simulator

v0.10.0b3

v0.10.0b3 Pre-release
Pre-release

Choose a tag to compare

@cayossarian cayossarian released this 09 Oct 21:46
d9efa40

Added

  • The reference capture main32_r202639-upstream-pv: a MAIN 32 on spanos3/r202639/03 with its PV inverters and battery upstream of the panel.

v0.10.0b2

v0.10.0b2 Pre-release
Pre-release

Choose a tag to compare

@cayossarian cayossarian released this 09 Oct 18:22
296ac54

Added

  • The reference panel captures ship in the wheel, with reference_capture_names() and load_reference_capture() to read them.

v0.10.0b1

v0.10.0b1 Pre-release
Pre-release

Choose a tag to compare

@cayossarian cayossarian released this 09 Oct 05:44
8440339

Added

  • spanos3/r202639/03 batch 2: reference captures and the variant that reproduces them.
  • spanos3/r202639/03 batch 2: span-alpha-test-b2 books each circuit's power by its feeds role, with solar as a circuit with the SOLAR role instead of a pv device.
  • A device's $description.name can differ from its name. A definition device's optional description_name (DeviceInstance.description_name) is published as its $description.name, while a circuit keeps publishing its name as info/name. panel-sim-capture writes it for a device the panel names by its own ID, and tests/fixtures/main32_r202639.yaml now carries each child device's ID there, as that capture does.

Changed

  • panel-sim-capture writes more of what a panel published: masking replaces the site's name, address, coordinates and utility meter serial with placeholders instead of dropping them and writes a reported Wi-Fi SSID as masked-ssid, each device gets a minimal unvalued list, and the off-grid import limit, the operator import limit's enablement and an EVSE's user limit are captured as off-grid-import-limit-enablement/off-grid-import-limit-a, operator-import-limit-enablement and user-max-charge-current-a.

Fixed

  • Published numbers use the panel's literal form (integer or fixed places, never -0.0). Each number the span variant publishes takes the form its profile property declares as literal (integer or <n>dp, rounded half away from zero, or shortest, unrounded): readings with one decimal and power-flows/grid, pv and battery as integers, as the MAIN 32 capture on release 202639 shows, and unrounded the numbers that capture shows no rounding rule for: the BESS nameplate-capacity and the PV nominal-power (integral samples only), the EVSE meter/advertised-current and pcs/off-grid-import-limit (no samples). A BESS meter/active-power published in the panel's frame is written as power-flows/battery is. The reference variant publishes numbers as before.
  • The span variant names a circuit's info/spaces property "Physical panel position(s) the circuit occupies", as a SPAN panel does.
  • The off-grid import limit is published only as a definition commissions it: its enablement from off-grid-import-limit-enablement, and the limit from off-grid-import-limit-a only while ENABLED, never a constant 0.0 with UNCONFIGURED.
  • An unconfigured pcs/requested-import-limit reads 200.0 in the span variants, as every public SPAN capture publishes it, instead of 0.0; the reference variant is unchanged.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 20:06
2dbddf7

BREAKING (wire). The BESS child's meter/active-power changes sign, unless the span variant's panel reports SPAN firmware before release 202639. A panel with no commissioned BESS no longer publishes shed or shed-forecast.

BREAKING (behavior). The emitter's own shed/asserted-islanding-state handler now behaves like a SPAN panel's: it accepts ON_GRID or OFF_GRID only while the panel's link to some battery is not known to be healthy, ignores everything else, NONE included, and clears an assertion once every link has been OK for 30 seconds of tick time. A tick that leaves out bess_communication reports every link OK, so against an unchanged producer an assertion written after the first tick, which used to be stored, is now ignored, as the panel does (#31, #60). A producer that registers its own handler is unaffected. Every published $description is unchanged.

Added

  • A producer can model the panel's link to each battery. TickInputs.bess_communication maps a configured BESS instance_id to OK, DEGRADED, LOST or UNKNOWN (the exported BESSCommunication); a battery left out is OK. The value is published as the battery's status/communication-state and drives EbusBatterySnapshot.connected and the connection status of the circuit or lugs that connects the battery. An id that is not a configured BESS, or a value outside BESSCommunication, raises EmitterStateError before anything in the tick is applied.
  • Panel definition files. PanelDefinition (manifest, variant, BESSConfigs, LoadSheddingConfig) is written and read as versioned YAML by dump_definition / load_definition, and Emitter.from_definition builds an emitter from one. Metadata is read exactly as written; config and tick fields are converted to their types and rejected when they do not fit.
  • panel-sim-capture writes a panel definition from a published panel tree, read live over MQTT or from a tree-v1 snapshot file, masking serials and IDs by default and reporting every value it had to default or could not express. Live capture verifies the broker with --cafile or explicitly skips it with --insecure, --root picks one enclosure on a shared broker, and --generic-names replaces circuit names with Circuit <n>. --record N also writes a live recording as replayable ticks, battery link state included, read back by load_ticks (and written by dump_ticks). ebus_panel_sim.capture exposes the steps as functions.
  • Circuit metadata key commissioned-system (pv | backup): the circuit a SPAN panel adds for a commissioned PV or battery system. It locks the relay and fixes load-shed/priority at NEVER, neither settable. Any other default-priority, or an unknown system, is rejected at construction. CircuitPhysics gains commissioned_system (default None) and a priority_locked property. panel-sim-capture maps such circuits back.
  • PanelPhysics.release_build: the SPAN release build the panel's firmware-version names (spanos2/r202633/02 is 202633), or None.
  • A profile overlay can remove what the base declares. A null capability or property in a profiles/span/*.json overlay removes it from the span variant, as in JSON Merge Patch (RFC 7396). Removing one the base lacks, or every property of a capability, raises ProfileValidationError.

Changed

  • The shipped example is a panel definition file and a tick file (examples/forty_tab_minimal.yaml, examples/forty_tab_minimal.ticks.yaml), loaded by load_definition / load_ticks; its own YAML profile format and loader are gone. Circuit IDs are now readable (kitchen-lights), PCS priorities are fixed in the file rather than numbered by position, the two circuits on the legacy NICE_TO_HAVE priority (published as UNKNOWN) are SOC_THRESHOLD and NEVER, and a load-shedding config plus a third, off-grid tick exercise shedding and relay-requester = LOAD_SHED.
  • BESS meter/active-power is positive while discharging. That is the battery's own frame, as devices/bess.md defines it, and makes it the negative of the panel's power-flows/battery (positive toward the battery). 0.6.0 made the two equal, matching SPAN firmware at the time; SPAN firmware makes the same reversal in release 202639. The span variant keeps the earlier sign when the panel's firmware-version names an earlier release (a /-separated segment r plus six digits, as in spanos2/r202633/02), while the reference variant, and a firmware string naming no release such as the example's example/v0.1.0, always publish the new one.
  • shed and shed-forecast are published only when at least one BESS is commissioned, as devices/distribution-enclosure.md specifies, in both variants.
  • config/user-max-charge-current is unpublished until a user sets it. A /set must be an integer and is clamped into [6, max-charge-current]; anything else is refused and the value left unchanged. $format is the per-EVSE range 6:<max-current-a>. Under the same firmware rule as the BESS sign, a panel on SPAN firmware before release 202639 publishes it at max-current-a until a user sets one, as that firmware did.
  • The example has two PV inverters, each on a commissioned-system: pv circuit and each IDed <panel>-<serial or model> (lowercased, non-alphanumerics as hyphens) with a per-inverter discriminator, as a SPAN panel IDs them.
  • The islanding assertion follows the battery link, as described above: accepted before the first tick when a battery is configured, since no link has been observed yet, and afterwards only while some battery's link is not OK; NONE and other values ignored; cleared after ASSERTION_CLEAR_AFTER_S (30 s) with every link OK. Any unhealthy tick restarts that wait.
  • A connection status reports the link the way the variant's panel does. The circuit or lugs connecting a battery publishes the link health on feeds-device-status or fed-by-device-status. The span variant reports DEGRADED and UNKNOWN there as LOST, as a SPAN panel does while still declaring the catalog's OK,LOST,DEGRADED; the reference variant publishes DEGRADED as is and only UNKNOWN, which no connection catalog declares, as LOST.
  • EbusBatterySnapshot.communication admits UNKNOWN, the fourth value of the status catalog's communication-state.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 05:03
171bb94

BREAKING (snapshot). EbusCircuitSnapshot.is_never_backup no longer means "priority is NEVER", and is_sheddable now requires a controllable relay. Against an unchanged manifest both flags flip for the circuits the old derivations got wrong: every NEVER-priority circuit stops reporting itself never-backup (correctly), and a relay-locked circuit at OFF_GRID/SOC_THRESHOLD stops reporting itself sheddable (also correctly). The published Homie tree is unchanged for any manifest that does not use the new never-backup metadata key; a manifest that does use it publishes one $settable fewer on load-shed/priority, and nothing else on the wire moves. CircuitPhysics gains a never_backup field, defaulted to False so existing constructions of that exported dataclass keep working.

Fixed

  • is_never_backup was derived from the priority value, which is inverted in both directions. never-backup is an installer commissioning lock — the circuit is commissioned to receive no backup power, so it is permanently OFF_GRID and its priority is not settable at all. NEVER is an ordinary value of that settable property, meaning "never shed", which a consumer chose and may unchoose. Deriving one from the other reported the lock on circuits that merely opt out of shedding, and reported no lock on the circuits that actually carry one — the emitter contradicting itself within a single tick, since the same circuit was published with a settable priority on the wire and reported as locked in the snapshot.

    The eBus schema migration guide separates the two per-input: "the three flat booleans are independent commissioning inputs stored as separate fields in each circuit's commissioning state, so the derivation is a simple per-input rule", with never-backup mapping onto one thing only — load-shed/priority's Homie $settable attribute, "published with $settable = !never-backup" — and the result described as "locked-priority circuits (commissioned permanently OFF_GRID) appear as priority = OFF_GRID, $settable = false; user-configurable circuits appear as $settable = true".

    A masked retained-topic capture of a production enclosure (16 circuits) refutes the derivation outright. Its load-shed/priority values are OFF_GRID ×10, SOC_THRESHOLD ×4 and NEVER ×2, and $settable is present and true on all 16 — including both NEVER circuits, which is a combination no value-derived flag can produce. Its 16 circuits are all relay-controllable, so it carries no locked circuit of either kind; the mixed panel is built in the tests instead.

    The commissioning input is now a per-circuit manifest key, never-backup (boolean, default false), beside relay-behavior/always-on. It is derived once in manifest_physics.never_backup — the same shape as relay_locked, read from raw metadata so the wire layer can ask the same question while building a $description, before any physics view exists — and read from every surface: the $settable declaration (omitted, not published false, matching a locked switch/relay and the read-only shed/policy; Homie 5 defaults the attribute false, so the two are the same claim), the /set path that declaration removes, the emitter's own /set handler, and the snapshot flag. The two commissioning locks are independent by construction — _INSTANCE_LOCKS in wire/graph_builder.py maps each per-circuit-settable property to its own flag — so locking a relay leaves the priority settable and locking a priority leaves the relay settable.

    A manifest that sets never-backup: true on a circuit whose default-priority is anything but OFF_GRID is rejected at construction rather than silently rewritten. The two statements are contradictory — a commissioned circuit is permanently OFF_GRID — and there is no way to publish both; having the lock quietly override the declared value would produce a published priority nobody wrote, which is the same class of defect this key exists to remove. Rejecting it keeps the published value the one the producer declared, and names the producer's mistake.

    A never-backup circuit still sheds, and that shed publishes switch/relay-requester = LOAD_SHED like any other. OFF_GRID is what the circuit is commissioned to, so islanding is when it opens; what the lock removes is the consumer's ability to re-prioritise it, which is a $settable question on load-shed/priority and not an attribution question on the relay. devices/distribution-enclosure.md states the requester rule without a carve-out — "when the enclosure's auto-shed logic drives a circuit's relay, the circuit publishes switch/relay-requester = LOAD_SHED" — and a consumer depends on it being exhaustive, since integration-guides/bess-and-distribution-enclosure.md defines restore as "circuits opened by LOAD_SHED are re-closed": a circuit shed under any other value would never come back on rejoin. RelayResolver is therefore untouched by this key. At rest, closed, the circuit reports NONE.

  • is_sheddable omitted the controllability half of its own definition. A relay-locked circuit at OFF_GRID reported itself sheddable while RelayResolver correctly refused to shed it — the emitter contradicting itself, since devices/distribution-enclosure.md has the enclosure "never open a circuit commissioned as permanently OFF_GRID / locked" and 0.7.0 made the shed path honour that. The flag now requires a controllable relay, which is the second conjunct the migration guide names for the retired flat sheddable and which the specification states directly: "a circuit's relay is opened by shedding only when it is controllable" (integration-guides/bess-and-distribution-enclosure.md).

    The priority half is the specification's own shed gate rather than the guide's. bess-and-distribution-enclosure.md defines shed(circuit) over exactly OFF_GRID and SOC_THRESHOLD, and says in the same section that "NEVER and UNKNOWN circuits are never auto-shed" — so this emitter's priority in ("OFF_GRID", "SOC_THRESHOLD"), which is also what native_devices/load_shedding.py acts on, matches it value for value. The guide's shorthand priority != NEVER does not: it counts UNKNOWN as sheddable, which the specification excludes by name. Adopting the shorthand would be a regression, not a completion, so the narrowing here is the finished rule and not a deferred half-fix.

    Twenty-four regression tests, of which seventeen fail on an unfixed checkout — and both modules collect there, since neither imports anything this change introduces. tests/test_never_backup_reproduction.py is the reproduction, kept in its own module and written against the pre-fix API alone so it can be lifted onto an unfixed checkout and fail on its assertion rather than at import: one ordinary circuit at default-priority: NEVER and no commissioning key anywhere, asserted in both variants against the snapshot flag and the published $description. On an unfixed checkout the wire half passes and the snapshot half fails — $settable = true and is_never_backup = true in the same tick, one circuit called settable and locked at once. tests/test_never_backup.py holds the other twenty-two, fifteen of which fail without the change, including test_settable_is_declared_exactly_when_a_circuit_is_not_never_backup, which asserts the invariant over a mixed panel the capture does not contain, and test_a_circuit_carrying_both_locks, which pins that the two commissioning flags compose. The shipped examples/forty_tab_minimal.yaml carried the defect: its solar template is priority: NEVER on a non-controllable relay — a PV backfeed circuit, the commonest real instance of this mix — so every circuit built from it reported itself never-backup, and none of them is. That example now also carries a pool_pump template commissioned never-backup: true, so the key is reachable from the end-to-end path the repo ships.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 00:06

BREAKING (wire). switch/relay no longer carries $settable on a circuit whose relay is locked, and such a circuit no longer accepts /set or participates in load-shed. A consumer that offered a relay control for every circuit will now correctly find none offered for the locked ones — which is what real firmware has always published.

Fixed

  • switch/relay was declared settable, honoured /set, and was load-shed on circuits whose own relay-controllable said the relay was locked. Four surfaces derived controllability separately and disagreed: the $settable declaration came from the per-class profile resolution (wire/graph_builder.py), so it could not vary per circuit and was always true; RelayResolver gated only on always_on, which manifest_physics derived so that a non-controllable circuit was not locked, leaving /set obeyed and shed applied; and switch/relay-requester reported NONE at rest where a locked circuit reports CONFIGURATION. Only the published relay-controllable value was right, and nothing consulted it.

    capabilities/switch.md makes relay settable "when relay-controllable" in its normative Settable column, and defines the flag as the relay being openable "by command or automatic shed". devices/distribution-enclosure.md states the same from the shed host's side — the enclosure "never opens a circuit commissioned as permanently OFF_GRID / locked" — so "sheddable but not settable" is not a state the specification permits, and one bit has to govern both paths. capabilities/load-shed.md and devices/circuit.md scope load-shed and pcs to a circuit "that also publishes a controllable switch".

    The fix derives that bit once, in manifest_physics.relay_locked, and reads it from every surface: the $settable declaration, the published relay-controllable, /set acceptance, load-shed participation, relay-requester, and pcs_managed. RelayResolver's public signature is unchanged — always_on is the hardware's own name for this flag, since SPAN commissions it as alwaysOn and publishes relay-controllable = !always-on.

    Two details the shape depends on. The predicate ORs the explicit always-on metadata rather than using it as a default: producers write the key out, so a clone of a real panel emits always-on: "false" beside relay-behavior: non-controllable, and a default chain would leave it unlocked. And $settable is omitted rather than published as false, matching what firmware does on a locked relay and on its read-only shed/policy alike; Homie 5 defaults the attribute to false, so the two are the same claim.

    Scoped to switch/relay alone. A locked circuit keeps load-shed/priority settable, because never-backup is a separate commissioning flag mapped to that property's own $settable — real panels publish exactly that combination.

    Found by diffing two masked retained-topic captures from production enclosures against this emitter's published tree in both variants. Across 27 circuits the panels hold the invariant without exception: $settable is present on switch/relay exactly when relay-controllable is true, and the two locked circuits report requester CONFIGURATION. Seventeen regression tests in tests/test_relay_controllability.py, of which fourteen fail without the change — including test_settable_is_declared_exactly_when_relay_controllable_is_published, which asserts that invariant over a mixed panel, and test_a_non_controllable_circuit_is_not_shed_off_grid, which is the shed half in executable form.

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 21 Aug 15:43
aa32a6a

No behaviour change: nothing this package publishes moves. The reason to release is the dependency ceiling, which is what reaches consumers.

Changed

  • ebus-sdk may now be 0.22. Floor unchanged; the ceiling moves from <0.22 to <0.23. Nothing here uses anything new, and the suite passes 223/223 against 0.22.0 unaltered. The release exists because the cap is what pins every consumer of this package: 0.6.0 declares <0.22 on PyPI, so anything depending on ebus-panel-sim is held below the current SDK until a release carries the raise. That is the same situation 0.5.2 existed to end, and it recurs on every SDK minor.

    0.22.0 carries DeviceTreeBuilder.add_root_capabilities() and .extend(), which the wire-layer rewrite needs, plus a fix for the builder replacing properties in a model it does not own and discarding live values, callbacks and setters.

  • Vendored power-flows catalog re-vendored to 0.3, lockfile re-pinned to specification 7ee7ca9. The specification qualified the negation table shortly after 0.6.0 shipped: power-flows/grid is the negation of the service lugs' meter/active-power only where those lugs are the utility connection point, which is false for an upstream DER (a battery between the utility and the main lugs) and for an enclosure chain. In both, the lugs measure panel-side flow while grid measures utility-side flow. No value this package publishes changes, because it already computed grid from the lugs and the BESS rather than by negating the lugs.

Fixed

  • The vendored-catalog check was measuring the wrong thing, and could not run in CI. It compared the vendored catalogs against whatever ../specification happened to be checked out at, so it failed when that clone moved ahead of the pin (ordinary currency drift, not a defect), would fail spuriously on an unrelated branch, and would pass falsely against a clone itself stale at the pinned commit. It also skipped without a sibling checkout, which is never present in CI, so the only check validating the vendored bytes never ran automatically.

    Both follow from .ebus-spec.json recording synced_commit as an exact anchor that nothing read. The check now reads the source out of git at that commit, making it deterministic and making it an integrity question: are the bytes we vendored the bytes we claim they are? CI gains a catalog-integrity job that checks out the specification at the pinned SHA and runs it, so the invariant is enforced rather than aspirational. A clone lacking the pinned commit now fails with an instruction to fetch rather than skipping, since a silent skip reads as a pass. Mutation-verified: tampering with a catalog names the file, and pointing synced_commit at the previous commit reports all ten catalogs that moved.

    Deliberately not a currency check. Whether the specification has moved past the pin is a separate question, normally answered "yes, a little", and must not fail a build.

  • The ebus-sdk pin carries its reason. The upper bound had no comment, so anyone hitting it had to re-derive whether it guarded a real incompatibility. Tracing every change to the dependency shows it never did: the bound has moved in lockstep with each floor bump since 0.12 and was never raised independently. It is a 0.x convention, since a minor release of a 0.x dependency may break API, and it is raised deliberately once the suite is verified against that minor.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 22:28
d1b4090

BREAKING (wire). This release changes the sign of values already being published. A consumer that stored or integrated any of them has records in the old frame that this release does not migrate. The affected properties are power-flows/pv, power-flows/grid, power-flows/battery, the BESS child's meter/active-power, and the downstream lugs' meter/imported-energy / meter/exported-energy. power-flows/site and the hosted-circuit frame are unchanged. The convention published here is now the one capabilities/power-flows.md 0.2 describes, so a consumer that was negating these values to compensate should stop.

Changed

  • Vendored spec catalogs and .ebus-spec.json re-pinned to specification 4085c68. Eight capability catalogs are re-vendored as byte copies (breaker 0.1 → 0.2, connection 0.1 → 0.2, grid 0.1 → 0.2, info 0.2 → 0.3, meter 0.2 → 0.4, power-flows 0.1 → 0.2, soc 0.1 → 0.2, switch 0.1 → 0.3), with twelve lockfile pins moved plus framework 0.7 → 0.9 and the device models distribution-enclosure 0.12 → 0.14, circuit 0.3 → 0.4, bess 0.14 → 0.15. power-flows 0.2 is the one that matters: it reverses the reference direction of grid, battery and pv so the four values form one node balance summing to zero, which is the frame the fixes below implement. Before this the package published that frame while shipping a catalog describing the opposite. The device-model bumps carry a requirement-level relaxation upstream; nothing this package publishes becomes non-conformant, because every change there was a relaxation and it already published all of the affected properties.

  • Five private tracker identifiers removed from the .ebus-spec.json notes field and replaced with descriptions of the work. That file ships in the sdist, so they were public-facing and meaningless to a reader outside the project.

Fixed

  • power-flows published three of its four values in the wrong sign frame, so they did not sum to zero. pv, grid and battery were emitted in the meter frame — positive while producing, importing and discharging — matching capabilities/power-flows.md 0.1 as literally written. Hardware does something else: a distribution enclosure publishes all four in the default reference direction capabilities/meter.md 0.2 later established (positive = power flowing into the metered thing), which is what makes the four terms one node balance. Measured on a live enclosure, hours apart: the four values close to within 1e-12 every time, while the frame this emitter used misses by exactly 2 × site. site was already correct and is unchanged — notably it is the one property the catalog describes without a directional verb.

    This is the half of the change that needs a maintainer decision, because the vendored wire/catalogs/power-flows.json is a byte copy of the spec and still states the old convention, so this emitter now contradicts a catalog it ships and cannot correct here. See the linked specification issue; the catalog re-vendor follows whatever it settles.

    Four tests fail without it, each mutation-verified one at a time: reverting pv fails 5 of the 9 test_power_flows_sum_to_zero cases, battery 6, grid 5. power_flow_grid stays derived from the lugs and the BESS rather than back-solved from the other three — a residual would satisfy the balance by construction and detect nothing.

  • _grid_power_from_lugs_and_bess clamped BESS charging to the visible PV surplus, hiding real grid import. The clamp existed so a charging BESS "never creates extra grid import", but self-consumption already charges from pv_surplus_w alone, so it never bound there — while backup-only deliberately charges from the utility, and the clamp deleted exactly that import from the reading. 500 W of load with a BESS pulling 1.5 kW and no array reported 500 W of import instead of 2 kW. It also put the node balance out by the amount hidden, which is how it was found. Now one subtraction in both directions.

  • Lugs energy registers integrated the circuits behind them instead of their own meter. active-power on a lugs device is the net through that point, but imported-energy / exported-energy were handed the gross sum of every circuit's energy — a different quantity. With 6 kW of load against 7 kW of PV the lugs carry ~1 kW in one direction, yet both registers advanced in the same tick. capabilities/meter.md calls imported-energy "the energy counterpart of positive active-power", and a counterpart that integrates a different signal is not one; integrating active-power would never have equalled imported - exported. A live enclosure never advances both at once. Lugs now register with EnergyIntegrator like any other metered point. test_lugs_energy_integrates_its_own_meter_not_the_circuits_behind_it fails without it: the old path reports 6000 Wh exported and 2000 Wh imported where 4000 Wh and 0 are correct.

  • BESS meter/active-power published the battery's own frame instead of the enclosure's, making it the exact inverse of power-flows/battery. The resolver returned EbusBatterySnapshot.active_power_w unchanged, which is positive while discharging. What an enclosure proxies for a battery it hosts is the enclosure's reading of that battery, not the battery's reading of itself — the mirror image, positive while charging, power leaving the enclosure node into the battery. That is the same quantity and the same sign as power-flows/battery, and shipping firmware publishes the two that way. The old behaviour had one device, describing one battery, at one instant, contradicting itself across two of its own properties. _bess_wire_active_power now mirrors it in the wire layer, alongside _circuit_wire_active_power, leaving the snapshot's device-frame meaning untouched for every other reader. test_bess_meter_active_power_matches_power_flows_battery asserts the identity across a load-only and a PV-surplus tick, and pins the absolute sign against the snapshot so it cannot pass by agreeing with itself.

    A standalone BESS elsewhere on the same bus still publishes its own meter in its own frame; that disagreement is correct, and this one was not.

Added

  • A Sign Frames section in AGENTS.md. Three of the four defects above are the same defect — a device-frame value published where the enclosure's mirror of it belongs — and none of them could be caught mechanically, because reference direction has no machine-readable form in the catalogs. Writing the rule down is currently the only thing standing between it and the next contributor. Includes the frame table, where the negation belongs (the wire layer, never the snapshot), and the node-balance invariant.

  • Coverage for the downstream lugs reference direction, which is inverted relative to the upstream lugs and was untested. Upstream lugs read positive when the utility delivers into the enclosure; downstream lugs read positive when the enclosure delivers out through the feedthrough, and imported-energy accrues on that outward direction. This emitter already behaved correctly; nothing asserted it, because the enclosure the published tree was diffed against has no feedthrough load, so both registers sat at zero and either sign would have passed. test_downstream_lugs_meter_is_positive_when_the_panel_feeds_the_subpanel pins it. No behaviour change.

Removed

  • connection/count, which no configuration could ever publish. Every circuit and both lugs devices declared it, and the snapshot fields behind it (EbusCircuitSnapshot.feeds_count, EbusLugsSnapshot.connection_count) were never assigned by any code path — so a consumer reading $description waited for a value that could not arrive. capabilities/connection.md scopes count to a node that "aggregates multiple physical units behind a single connection point"; this emitter models every DER as its own device and aggregates nothing, so the property has nothing to describe here rather than merely being unpopulated. The declaration, the two bag_builder resolvers and the two dataclass fields all go. Populating it instead would need a manifest key and is a feature, not this fix. test_every_declared_connection_property_can_be_published guards it.

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 20 Aug 22:17
d24c6ed

Changed

  • ebus-sdk may now be 0.21. The floor is unchanged; only the upper bound moves, from <0.21 to <0.22. Nothing in this package uses anything new — the change exists because the cap was what pinned every consumer of this package below 0.21, and 0.5.1 is the only published version, so there was no way to depend on ebus-panel-sim and be on the current SDK at once. 0.21.0 is where DeviceSpec and DeviceTreeBuilder arrive and where mqtt_cfg=None becomes the documented passive mode, so a consumer wanting either could not have it.

    The bound was not load-bearing: relaxing it and changing nothing else, the suite passes 207/207 on 0.21.0. Taken as a conservative bound at the 0.20.1 floor rather than a recorded incompatibility — if it guarded something the suite does not reach, that is worth writing into the pin comment, which currently carries no reason.

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 17 Aug 17:21
b561d06

Changed

  • Ungraceful teardown goes through ebus-sdk's Device.declare_lost() instead of a local approximation. wire/_sdk_seam.py's publish_will_now existed only because the SDK modelled three teardowns and implemented one: a simulator asked to act like a producer that died could not get there by letting the Last Will fire, because the will fires only on an unclean disconnect and every teardown here closes cleanly. That gap is closed upstream as of ebus-sdk 0.20.0, so the approximation is now duplication, and duplication of a teardown path is exactly where drift is expensive. The function is deleted; _sdk_seam.py loses 78 lines.

    Observably identical on the injected path and strictly better on the owned one. Measured rather than asserted: on an injected transport the retained $state, the device's own state, the single retained publish and the value a later refresh_tree() re-announces are all unchanged. On an owned client the teardown now emits one publish to $state where it emitted two, because the old path published via set_state and then published a deliberate byte-identical duplicate to get a flushed one; declare_lost() publishes once, flushed. The first of that old pair also carried the DeviceState enum rather than its .value, which the second corrected.

    will_for_root_id stays, and is not the same kind of thing: Emitter.lwt_settings() has to answer "what will should I register" before a Device exists, since the will rides the CONNECT packet, and the SDK's Device.will() is an instance method. owned_client stays too, since the ungraceful path still decides whether to stop a client it owns.

  • Floor ebus-sdk at 0.20.1 (from >=0.19,<0.20). 0.20.0 added Device.declare_lost() and Device.stop(announce=False), the teardown API this package previously reached around the SDK to approximate, and 0.20.1 fixed two thread-safety defects in the publish path that this package's per-tick set_value pattern is exposed to. 0.20.0 also stopped republishing a retained value whose wire payload is byte-identical to the last one sent, which overlaps with the diff-only publishing done here: strictly fewer messages, same retained truth. Verified rather than assumed: the full retained tree published by a panel + circuit + BESS manifest over three ticks is byte-identical across the bump, 69 topics either way, once the $description version timestamp is normalised. Retiring the local approximation in favour of declare_lost() follows below.

  • tests/test_wire_units.py no longer enumerates what it checks. The file whose docstring is this project's canonical account of this defect was itself an instance of it: its unit assertion looped a hand-written six-element tuple while the shipped profiles carry seven, so kWh was already outside the guard, and the datatype axis of the same conversion pair had no test at all. Both are now swept from load_profiles() over both variants, a third sweep covers every datatype the vendored catalogs define (which is what would have caught the datetime case while it was still only vendored), and an end-to-end check asserts every $datatype on the wire is one some profile declares. The original six-unit case is kept as a named regression for the lc3 conformance failure, but it is no longer the coverage claim. Found by an audit for the same fault elsewhere; the remaining candidates are tracked separately.

Fixed

  • _to_sdk_datatype resolved Homie datatypes through a hand-maintained name table, so three of the SDK's nine published as string. The table listed six datatypes and sent everything else through a silent .get(dt, STRING) default, which meant datetime, duration and color were converted to the wrong $datatype. It now resolves by value (PropertyDatatype(dt.lower())), correct by construction for every datatype the SDK models now or later. Nothing shipped was affected: no profile currently selects a property carrying one of the three, so this was latent rather than live. It was not hypothetical either. The vendored catalogs/grid.json has always carried two datetime properties (last-outage-time, last-restoration-time), and the one-line light selection that is the documented way to add a property would have published them as string with the full suite still green.
  • An unmodelled datatype now raises instead of guessing. $datatype is a required Homie 5 attribute that consumers validate payloads against, so defaulting to string yields a tree that is confidently wrong rather than obviously broken. This differs deliberately from _to_sdk_unit, where an unmodelled unit still resolves to None and is simply omitted: a missing $unit is legal, a wrong $datatype is not. The error names the offending entity_class capability/property and lists what the SDK models.