Repository navigation
Releases: electrification-bus/distribution-enclosure-simulator
Release list
v0.10.0b3
Added
- The reference capture
main32_r202639-upstream-pv: a MAIN 32 onspanos3/r202639/03with its PV inverters and battery upstream of the panel.
v0.10.0b2
Added
- The reference panel captures ship in the wheel, with
reference_capture_names()andload_reference_capture()to read them.
v0.10.0b1
Added
- spanos3/r202639/03 batch 2: reference captures and the variant that reproduces them.
- spanos3/r202639/03 batch 2:
span-alpha-test-b2books each circuit's power by its feeds role, with solar as a circuit with theSOLARrole instead of apvdevice. - A device's
$description.namecan differ from its name. A definition device's optionaldescription_name(DeviceInstance.description_name) is published as its$description.name, while a circuit keeps publishing itsnameasinfo/name.panel-sim-capturewrites it for a device the panel names by its own ID, andtests/fixtures/main32_r202639.yamlnow carries each child device's ID there, as that capture does.
Changed
panel-sim-capturewrites 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 asmasked-ssid, each device gets a minimalunvaluedlist, and the off-grid import limit, the operator import limit's enablement and an EVSE's user limit are captured asoff-grid-import-limit-enablement/off-grid-import-limit-a,operator-import-limit-enablementanduser-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 asliteral(integeror<n>dp, rounded half away from zero, orshortest, unrounded): readings with one decimal andpower-flows/grid,pvandbatteryas integers, as the MAIN 32 capture on release 202639 shows, and unrounded the numbers that capture shows no rounding rule for: the BESSnameplate-capacityand the PVnominal-power(integral samples only), the EVSEmeter/advertised-currentandpcs/off-grid-import-limit(no samples). A BESSmeter/active-powerpublished in the panel's frame is written aspower-flows/batteryis. The reference variant publishes numbers as before. - The span variant names a circuit's
info/spacesproperty "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 fromoff-grid-import-limit-aonly whileENABLED, never a constant0.0withUNCONFIGURED. - An unconfigured
pcs/requested-import-limitreads200.0in the span variants, as every public SPAN capture publishes it, instead of0.0; the reference variant is unchanged.
v0.9.0
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_communicationmaps a configured BESSinstance_idtoOK,DEGRADED,LOSTorUNKNOWN(the exportedBESSCommunication); a battery left out isOK. The value is published as the battery'sstatus/communication-stateand drivesEbusBatterySnapshot.connectedand the connection status of the circuit or lugs that connects the battery. An id that is not a configured BESS, or a value outsideBESSCommunication, raisesEmitterStateErrorbefore anything in the tick is applied. - Panel definition files.
PanelDefinition(manifest, variant,BESSConfigs,LoadSheddingConfig) is written and read as versioned YAML bydump_definition/load_definition, andEmitter.from_definitionbuilds 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-capturewrites a panel definition from a published panel tree, read live over MQTT or from atree-v1snapshot 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--cafileor explicitly skips it with--insecure,--rootpicks one enclosure on a shared broker, and--generic-namesreplaces circuit names withCircuit <n>.--record Nalso writes a live recording as replayable ticks, battery link state included, read back byload_ticks(and written bydump_ticks).ebus_panel_sim.captureexposes 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 fixesload-shed/priorityatNEVER, neither settable. Any otherdefault-priority, or an unknown system, is rejected at construction.CircuitPhysicsgainscommissioned_system(defaultNone) and apriority_lockedproperty.panel-sim-capturemaps such circuits back. PanelPhysics.release_build: the SPAN release build the panel'sfirmware-versionnames (spanos2/r202633/02is202633), orNone.- A profile overlay can remove what the base declares. A
nullcapability or property in aprofiles/span/*.jsonoverlay removes it from the span variant, as in JSON Merge Patch (RFC 7396). Removing one the base lacks, or every property of a capability, raisesProfileValidationError.
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 byload_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 legacyNICE_TO_HAVEpriority (published asUNKNOWN) areSOC_THRESHOLDandNEVER, and a load-shedding config plus a third, off-grid tick exercise shedding andrelay-requester = LOAD_SHED. - BESS
meter/active-poweris positive while discharging. That is the battery's own frame, asdevices/bess.mddefines it, and makes it the negative of the panel'spower-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'sfirmware-versionnames an earlier release (a/-separated segmentrplus six digits, as inspanos2/r202633/02), while the reference variant, and a firmware string naming no release such as the example'sexample/v0.1.0, always publish the new one. shedandshed-forecastare published only when at least one BESS is commissioned, asdevices/distribution-enclosure.mdspecifies, in both variants.config/user-max-charge-currentis unpublished until a user sets it. A/setmust be an integer and is clamped into[6, max-charge-current]; anything else is refused and the value left unchanged.$formatis the per-EVSE range6:<max-current-a>. Under the same firmware rule as the BESS sign, a panel on SPAN firmware before release 202639 publishes it atmax-current-auntil a user sets one, as that firmware did.- The example has two PV inverters, each on a
commissioned-system: pvcircuit 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;NONEand other values ignored; cleared afterASSERTION_CLEAR_AFTER_S(30 s) with every linkOK. 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-statusorfed-by-device-status. The span variant reportsDEGRADEDandUNKNOWNthere asLOST, as a SPAN panel does while still declaring the catalog'sOK,LOST,DEGRADED; the reference variant publishesDEGRADEDas is and onlyUNKNOWN, which no connection catalog declares, asLOST. EbusBatterySnapshot.communicationadmitsUNKNOWN, the fourth value of thestatuscatalog'scommunication-state.
v0.8.0
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_backupwas derived from the priority value, which is inverted in both directions.never-backupis an installer commissioning lock — the circuit is commissioned to receive no backup power, so it is permanentlyOFF_GRIDand its priority is not settable at all.NEVERis 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-backupmapping onto one thing only —load-shed/priority's Homie$settableattribute, "published with$settable = !never-backup" — and the result described as "locked-priority circuits (commissioned permanentlyOFF_GRID) appear aspriority = 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/priorityvalues areOFF_GRID×10,SOC_THRESHOLD×4 andNEVER×2, and$settableis present andtrueon all 16 — including bothNEVERcircuits, which is a combination no value-derived flag can produce. Its 16 circuits are allrelay-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), besiderelay-behavior/always-on. It is derived once inmanifest_physics.never_backup— the same shape asrelay_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$settabledeclaration (omitted, not publishedfalse, matching a lockedswitch/relayand the read-onlyshed/policy; Homie 5 defaults the attribute false, so the two are the same claim), the/setpath that declaration removes, the emitter's own/sethandler, and the snapshot flag. The two commissioning locks are independent by construction —_INSTANCE_LOCKSinwire/graph_builder.pymaps 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: trueon a circuit whosedefault-priorityis anything butOFF_GRIDis rejected at construction rather than silently rewritten. The two statements are contradictory — a commissioned circuit is permanentlyOFF_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_SHEDlike any other.OFF_GRIDis 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$settablequestion onload-shed/priorityand not an attribution question on the relay.devices/distribution-enclosure.mdstates the requester rule without a carve-out — "when the enclosure's auto-shed logic drives a circuit's relay, the circuit publishesswitch/relay-requester = LOAD_SHED" — and a consumer depends on it being exhaustive, sinceintegration-guides/bess-and-distribution-enclosure.mddefines restore as "circuits opened byLOAD_SHEDare re-closed": a circuit shed under any other value would never come back on rejoin.RelayResolveris therefore untouched by this key. At rest, closed, the circuit reportsNONE. -
is_sheddableomitted the controllability half of its own definition. A relay-locked circuit atOFF_GRIDreported itself sheddable whileRelayResolvercorrectly refused to shed it — the emitter contradicting itself, sincedevices/distribution-enclosure.mdhas the enclosure "never open a circuit commissioned as permanentlyOFF_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 flatsheddableand 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.mddefinesshed(circuit)over exactlyOFF_GRIDandSOC_THRESHOLD, and says in the same section that "NEVERandUNKNOWNcircuits are never auto-shed" — so this emitter'spriority in ("OFF_GRID", "SOC_THRESHOLD"), which is also whatnative_devices/load_shedding.pyacts on, matches it value for value. The guide's shorthandpriority != NEVERdoes not: it countsUNKNOWNas 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.pyis 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 atdefault-priority: NEVERand 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 = trueandis_never_backup = truein the same tick, one circuit called settable and locked at once.tests/test_never_backup.pyholds the other twenty-two, fifteen of which fail without the change, includingtest_settable_is_declared_exactly_when_a_circuit_is_not_never_backup, which asserts the invariant over a mixed panel the capture does not contain, andtest_a_circuit_carrying_both_locks, which pins that the two commissioning flags compose. The shippedexamples/forty_tab_minimal.yamlcarried the defect: itssolartemplate ispriority: NEVERon 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 apool_pumptemplate commissionednever-backup: true, so the key is reachable from the end-to-end path the repo ships.
v0.7.0
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/relaywas declared settable, honoured/set, and was load-shed on circuits whose ownrelay-controllablesaid the relay was locked. Four surfaces derived controllability separately and disagreed: the$settabledeclaration came from the per-class profile resolution (wire/graph_builder.py), so it could not vary per circuit and was alwaystrue;RelayResolvergated only onalways_on, whichmanifest_physicsderived so that anon-controllablecircuit was not locked, leaving/setobeyed and shed applied; andswitch/relay-requesterreportedNONEat rest where a locked circuit reportsCONFIGURATION. Only the publishedrelay-controllablevalue was right, and nothing consulted it.capabilities/switch.mdmakesrelaysettable "whenrelay-controllable" in its normative Settable column, and defines the flag as the relay being openable "by command or automatic shed".devices/distribution-enclosure.mdstates the same from the shed host's side — the enclosure "never opens a circuit commissioned as permanentlyOFF_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.mdanddevices/circuit.mdscopeload-shedandpcsto a circuit "that also publishes a controllableswitch".The fix derives that bit once, in
manifest_physics.relay_locked, and reads it from every surface: the$settabledeclaration, the publishedrelay-controllable,/setacceptance, load-shed participation,relay-requester, andpcs_managed.RelayResolver's public signature is unchanged —always_onis the hardware's own name for this flag, since SPAN commissions it asalwaysOnand publishesrelay-controllable = !always-on.Two details the shape depends on. The predicate ORs the explicit
always-onmetadata rather than using it as a default: producers write the key out, so a clone of a real panel emitsalways-on: "false"besiderelay-behavior: non-controllable, and a default chain would leave it unlocked. And$settableis omitted rather than published asfalse, matching what firmware does on a locked relay and on its read-onlyshed/policyalike; Homie 5 defaults the attribute to false, so the two are the same claim.Scoped to
switch/relayalone. A locked circuit keepsload-shed/prioritysettable, 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:
$settableis present onswitch/relayexactly whenrelay-controllableistrue, and the two locked circuits report requesterCONFIGURATION. Seventeen regression tests intests/test_relay_controllability.py, of which fourteen fail without the change — includingtest_settable_is_declared_exactly_when_relay_controllable_is_published, which asserts that invariant over a mixed panel, andtest_a_non_controllable_circuit_is_not_shed_off_grid, which is the shed half in executable form.
v0.6.1
No behaviour change: nothing this package publishes moves. The reason to release is the dependency ceiling, which is what reaches consumers.
Changed
-
ebus-sdkmay now be 0.22. Floor unchanged; the ceiling moves from<0.22to<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.22on PyPI, so anything depending onebus-panel-simis 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-flowscatalog re-vendored to 0.3, lockfile re-pinned to specification7ee7ca9. The specification qualified the negation table shortly after 0.6.0 shipped:power-flows/gridis the negation of the service lugs'meter/active-poweronly 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 whilegridmeasures 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
../specificationhappened 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.jsonrecordingsynced_commitas 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 acatalog-integrityjob 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 pointingsynced_commitat 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-sdkpin 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
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.jsonre-pinned to specification4085c68. Eight capability catalogs are re-vendored as byte copies (breaker0.1 → 0.2,connection0.1 → 0.2,grid0.1 → 0.2,info0.2 → 0.3,meter0.2 → 0.4,power-flows0.1 → 0.2,soc0.1 → 0.2,switch0.1 → 0.3), with twelve lockfile pins moved plusframework0.7 → 0.9 and the device modelsdistribution-enclosure0.12 → 0.14,circuit0.3 → 0.4,bess0.14 → 0.15.power-flows0.2 is the one that matters: it reverses the reference direction ofgrid,batteryandpvso 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.jsonnotesfield 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-flowspublished three of its four values in the wrong sign frame, so they did not sum to zero.pv,gridandbatterywere emitted in the meter frame — positive while producing, importing and discharging — matchingcapabilities/power-flows.md0.1 as literally written. Hardware does something else: a distribution enclosure publishes all four in the default reference directioncapabilities/meter.md0.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 exactly2 × site.sitewas 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.jsonis 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
pvfails 5 of the 9test_power_flows_sum_to_zerocases,battery6,grid5.power_flow_gridstays 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_bessclamped BESS charging to the visible PV surplus, hiding real grid import. The clamp existed so a charging BESS "never creates extra grid import", butself-consumptionalready charges frompv_surplus_walone, so it never bound there — whilebackup-onlydeliberately 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-poweron a lugs device is the net through that point, butimported-energy/exported-energywere 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.mdcallsimported-energy"the energy counterpart of positiveactive-power", and a counterpart that integrates a different signal is not one; integratingactive-powerwould never have equalledimported - exported. A live enclosure never advances both at once. Lugs now register withEnergyIntegratorlike any other metered point.test_lugs_energy_integrates_its_own_meter_not_the_circuits_behind_itfails without it: the old path reports 6000 Wh exported and 2000 Wh imported where 4000 Wh and 0 are correct. -
BESS
meter/active-powerpublished the battery's own frame instead of the enclosure's, making it the exact inverse ofpower-flows/battery. The resolver returnedEbusBatterySnapshot.active_power_wunchanged, 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 aspower-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_powernow 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_batteryasserts 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 Framessection inAGENTS.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-energyaccrues 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_subpanelpins 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$descriptionwaited for a value that could not arrive.capabilities/connection.mdscopescountto 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 twobag_builderresolvers 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_publishedguards it.
v0.5.2
Changed
-
ebus-sdkmay now be 0.21. The floor is unchanged; only the upper bound moves, from<0.21to<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 onebus-panel-simand be on the current SDK at once. 0.21.0 is whereDeviceSpecandDeviceTreeBuilderarrive and wheremqtt_cfg=Nonebecomes 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
Changed
-
Ungraceful teardown goes through ebus-sdk's
Device.declare_lost()instead of a local approximation.wire/_sdk_seam.py'spublish_will_nowexisted 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 ofebus-sdk0.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.pyloses 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 laterrefresh_tree()re-announces are all unchanged. On an owned client the teardown now emits one publish to$statewhere it emitted two, because the old path published viaset_stateand 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 theDeviceStateenum rather than its.value, which the second corrected.will_for_root_idstays, and is not the same kind of thing:Emitter.lwt_settings()has to answer "what will should I register" before aDeviceexists, since the will rides the CONNECT packet, and the SDK'sDevice.will()is an instance method.owned_clientstays too, since the ungraceful path still decides whether to stop a client it owns. -
Floor
ebus-sdkat 0.20.1 (from>=0.19,<0.20). 0.20.0 addedDevice.declare_lost()andDevice.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-tickset_valuepattern 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$descriptionversiontimestamp is normalised. Retiring the local approximation in favour ofdeclare_lost()follows below. -
tests/test_wire_units.pyno 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, sokWhwas already outside the guard, and the datatype axis of the same conversion pair had no test at all. Both are now swept fromload_profiles()over both variants, a third sweep covers every datatype the vendored catalogs define (which is what would have caught thedatetimecase while it was still only vendored), and an end-to-end check asserts every$datatypeon 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_datatyperesolved Homie datatypes through a hand-maintained name table, so three of the SDK's nine published asstring. The table listed six datatypes and sent everything else through a silent.get(dt, STRING)default, which meantdatetime,durationandcolorwere 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 vendoredcatalogs/grid.jsonhas always carried twodatetimeproperties (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 asstringwith the full suite still green.- An unmodelled datatype now raises instead of guessing.
$datatypeis a required Homie 5 attribute that consumers validate payloads against, so defaulting tostringyields a tree that is confidently wrong rather than obviously broken. This differs deliberately from_to_sdk_unit, where an unmodelled unit still resolves toNoneand is simply omitted: a missing$unitis legal, a wrong$datatypeis not. The error names the offendingentity_class capability/propertyand lists what the SDK models.