ebus-panel-sim 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.github/workflows/ci.yaml +6 -1
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/CHANGELOG.md +25 -0
- ebus_panel_sim-0.3.2/PKG-INFO +203 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/README.md +7 -6
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/pyproject.toml +27 -1
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/__init__.py +1 -1
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/emitter.py +21 -5
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/_sdk_seam.py +34 -1
- ebus_panel_sim-0.3.2/tests/test_teardown.py +73 -0
- ebus_panel_sim-0.3.0/PKG-INFO +0 -14
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.ebus-spec.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.github/CODEOWNERS +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.github/workflows/publish.yml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.gitignore +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.pre-commit-config.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/.python-version +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/AUTHORS +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/CONTRIBUTING.md +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/DESIGN.md +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/DEVELOPER.md +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/LICENSE +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/examples/forty_tab_minimal.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/examples/run_forty_tab_minimal.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/conventions/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/conventions/tab_legs.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/energy_integrator.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/exceptions.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/manifest.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/manifest_physics.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/native_devices/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/native_devices/bess.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/native_devices/load_shedding.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/native_devices/protocol.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/panel_meter.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/py.typed +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/relay_resolver.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/snapshot.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/tick_inputs.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/bag_builder.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/breaker.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/charge-limit.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/connection.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/door.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/grid.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/info.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/load-shed.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/meter.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/pcs.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/power-flows.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/shed-forecast.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/shed.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/soc.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/status.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/switch.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/graph_builder.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/.gitkeep +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/bess.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/circuit.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/evse.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/lugs.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/mid.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/panel.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping/pv.yaml +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/mapping_loader.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profile_loader.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/.gitkeep +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/bess.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/circuit.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/evse.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/lugs.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/mid.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/panel.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/pv.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/circuit.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/evse.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/lugs.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/panel.json +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/property_bag.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/publisher.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/set_router.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/conftest.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/conventions/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/conventions/test_tab_legs.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_catalog_drift.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_connection.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_emitter_public_surface.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_energy_integrator.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_exceptions.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_manifest.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_manifest_physics.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_mid_placement.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_panel_meter.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_publish_tick.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_relay_resolver.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_shed_forecast.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_tick_inputs.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_variant.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/test_wire_units.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/__init__.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_circuit_energy_frame.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_graph_builder.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_graph_builder_topology.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_profile_mapping_validation.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_property_bag.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_sdk_seam.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/tests/wire/test_set_router.py +0 -0
- {ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/uv.lock +0 -0
|
@@ -3,8 +3,13 @@ name: CI
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
|
+
# Deliberately unfiltered by base branch. A `branches:` filter here means a PR
|
|
7
|
+
# targeting anything other than `main` gets no checks at all, silently: the PR
|
|
8
|
+
# simply reports "no checks reported" and reads as unvalidated rather than
|
|
9
|
+
# failing. That hides every stacked PR, which is exactly when a second pair of
|
|
10
|
+
# eyes is worth most. Every other repo in the eBus family already runs
|
|
11
|
+
# unfiltered; this one was the outlier.
|
|
6
12
|
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
13
|
|
|
9
14
|
jobs:
|
|
10
15
|
lint-and-test:
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.2] - 2026-08-07
|
|
6
|
+
|
|
7
|
+
Metadata-only. No source changes, so the published tree and the public API are identical to 0.3.1; this release exists to get the packaging metadata below onto PyPI, where it only takes effect on a publish.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The PyPI project page was blank.** `pyproject.toml` declared no `readme`, so the published metadata carried no description at all: the page showed the summary line and nothing else. It now carries the README (`description_content_type: text/markdown`). The README's repo-relative links (`DESIGN.md`, `DEVELOPER.md`, `CONTRIBUTING.md`, `LICENSE`, `AUTHORS`) are absolutised, since those resolve on GitHub but 404 when the same Markdown is rendered on PyPI.
|
|
12
|
+
- **No licence was declared.** The README badge claimed MIT and a `LICENSE` file was present, but nothing said so in the metadata. Now `license = "MIT"` (an SPDX expression) with `license-files = ["LICENSE"]`, so the wheel carries `License-Expression: MIT` and `License-File: LICENSE`. No `License :: OSI Approved` classifier is paired with it, per PEP 639.
|
|
13
|
+
- **No classifiers.** PyPI indexed nothing about the package. Now covers 3.11 through 3.14, audience, topic, and `Typing :: Typed` — the last being how PyPI surfaces the `py.typed` marker shipped in 0.3.0, on the page and in the "Typed" search filter.
|
|
14
|
+
- The `hatchling` build requirement is floored at `>=1.27`, the version that supports PEP 639. Below it the build **succeeds** and quietly emits legacy metadata instead (measured on 1.26.3: `License: MIT`, no `License-Expression`, no `License-File`), so an unpinned backend would have degraded the published artifact without failing anything. ([#15](https://github.com/electrification-bus/distribution-enclosure-simulator/pull/15))
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The README's Python badge is now the dynamic `pypi/pyversions` one used across the eBus family, replacing a hardcoded `3.11+`. It reads classifiers from the published release, so it could not be adopted until this release carried them.
|
|
19
|
+
|
|
20
|
+
## [0.3.1] - 2026-08-07
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`Emitter.stop(graceful=False)` now leaves the tree at `$state=lost`, as it always claimed to.** It documented itself as "leaving the LWT to fire `$state=lost`", which cannot happen: a Last Will fires only on an *unclean* disconnect, and `MqttClient.stop()` sends a clean DISCONNECT deliberately, so that an orderly shutdown is not reported to consumers as a crash. The will was therefore suppressed on every teardown, and an ungraceful stop published nothing at all: verified against a real broker, a consumer joining afterwards read the entire retained tree as `ready`, indefinitely. For a simulator this is the mode's whole purpose, since "act like a producer that died" is exactly what a consumer test needs, so the fix makes the behaviour real rather than deleting the promise. The root's `$state=lost` is now published retained before the connection drops, with topic and payload taken from the SDK's own `Device.will()` descriptor so it cannot drift from what a broker-delivered will would have carried. The difference from a real will is timing and delivery, not content: this lands immediately over the live connection, where a broker-delivered will waits on keepalive expiry, so a consumer exercising the *retained* view sees the same thing either way while one exercising live will delivery does not. `clear_retained` is documented as graceful-only, since a producer that died clears nothing.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `tests/test_teardown.py`: both teardown modes now have tests, which neither had before. That absence is why a docstring could promise behaviour the code had never performed. Three of the five fail against 0.3.0.
|
|
29
|
+
|
|
5
30
|
## [0.3.0] - 2026-08-07
|
|
6
31
|
|
|
7
32
|
First release published to PyPI, as **`ebus-panel-sim`**.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ebus-panel-sim
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Producer-side Homie publisher with embedded behaviour runtime for the eBus convention
|
|
5
|
+
Project-URL: Homepage, https://ebus.energy
|
|
6
|
+
Project-URL: Repository, https://github.com/electrification-bus/distribution-enclosure-simulator
|
|
7
|
+
Project-URL: Issues, https://github.com/electrification-bus/distribution-enclosure-simulator/issues
|
|
8
|
+
Author: Bill Flood, Clark Communications Corporation
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: distribution-enclosure,ebus,energy,homie,homie5,iot,mqtt,simulator,span
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Classifier: Topic :: System :: Emulators
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: ebus-sdk<0.20,>=0.19
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ebus-panel-sim
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/ebus-panel-sim/)
|
|
30
|
+
[](https://github.com/electrification-bus/distribution-enclosure-simulator/actions/workflows/ci.yaml)
|
|
31
|
+
[](https://pypi.org/project/ebus-panel-sim/)
|
|
32
|
+
[](https://github.com/astral-sh/ruff)
|
|
33
|
+
[](https://github.com/electrification-bus/specification/tree/6e582c994fff4c77853a79d8bab26ef9924e22c7)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
|
|
36
|
+
A fully-loaded, spec-conformant **distribution-enclosure simulator** and producer-side Homie 5 publisher for the eBus convention. It publishes a complete eBus Homie device tree (the enclosure plus a device for every circuit, lugs pair, and integrated DER: BESS, PV, EVSE, and MID) so external developers can build and test their consumers against a realistic SPAN-like panel without beta firmware, a live panel, or the commissioned add-ons (SPAN Drive/EVSE, BESS, PV, MID) a real installation would have.
|
|
37
|
+
|
|
38
|
+
It serves two roles:
|
|
39
|
+
|
|
40
|
+
- **Simulator / test fixture.** Drive it from a small YAML definition and it publishes a spec-conformant, fully-commissioned enclosure to any MQTT broker. Consumers (Home Assistant integrations, dashboards, SDK code) validate against it before shipping to the field.
|
|
41
|
+
- **Producer library.** The canonical eBus Homie publisher. A producer (a simulator, a real panel gateway, an LLM-driven model) hands the emitter a small per-tick driving signal (signed power per circuit, current time, grid-online flag) via `TickInputs`; the emitter derives all telemetry and publishes Homie-conformant retained MQTT with diff-only updates. The split is **identity = manifest (once at startup), telemetry = derived from TickInputs (per tick)**.
|
|
42
|
+
|
|
43
|
+
For the internals (the per-tick pipeline, the native BESS/load-shed devices, `/set` handling, the wire model) see [DESIGN.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DESIGN.md); for the dev setup see [DEVELOPER.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DEVELOPER.md).
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Python >= 3.11
|
|
48
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
49
|
+
- An MQTT broker reachable at `localhost:1883` (plaintext). The companion [broker-quickstart](https://github.com/electrification-bus/broker-quickstart) bundle brings one up in one command; any `mosquitto` works too.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install ebus-panel-sim # or: uv add ebus-panel-sim
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The import package is `ebus_panel_sim`. During local development, pin a path instead:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
ebus-panel-sim = { path = "../distribution-enclosure-simulator", editable = true }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
It depends on `ebus-sdk`.
|
|
64
|
+
|
|
65
|
+
> Before 0.3.0 this package was named `panel-sim`, importing as `panel_sim`, and was
|
|
66
|
+
> installable only from git. Update both the dependency and your imports.
|
|
67
|
+
|
|
68
|
+
## Run
|
|
69
|
+
|
|
70
|
+
The repo ships a runnable example: it builds an emitter from a YAML definition, publishes a couple of ticks to an MQTT broker, then reads the retained tree back through an ebus-sdk `Controller` and prints it. It expects a plaintext broker on `localhost:1883`.
|
|
71
|
+
|
|
72
|
+
The quickest broker is the companion [broker-quickstart](https://github.com/electrification-bus/broker-quickstart) in its `open` profile (plaintext, anonymous, port 1883):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# in a broker-quickstart checkout — a plaintext :1883 broker (anon read + write)
|
|
76
|
+
python -m laptop.run --profile open
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then, in this repo, publish to it and print the retained tree:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv sync --group dev
|
|
83
|
+
uv run python examples/run_forty_tab_minimal.py # print the retained tree
|
|
84
|
+
uv run python examples/run_forty_tab_minimal.py --broker 127.0.0.1:1883 --ticks 2 > tree.txt
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Any broker that accepts anonymous connections on `localhost:1883` works; `--broker host:port` points the example elsewhere.
|
|
88
|
+
|
|
89
|
+
The definition is `examples/forty_tab_minimal.yaml`: a fully-commissioned enclosure with circuits, upstream/downstream lugs, a BESS (plus its MID), PV, and SPAN Drive EVSEs. Each node is its own Homie device: the enclosure at `ebus/5/<enclosure-id>/…` and each circuit, lugs pair, and DER at its own topic root, for example `ebus/5/<circuit-id>/switch/relay`, `ebus/5/<lugs-id>/meter/current-a`, `ebus/5/<bess-id>-mid/grid/islanding-state`.
|
|
90
|
+
|
|
91
|
+
## Configure
|
|
92
|
+
|
|
93
|
+
The simulator is driven by a config that says which enclosure, which add-ons, and which circuits. There are two entry points.
|
|
94
|
+
|
|
95
|
+
### 1. Example YAML
|
|
96
|
+
|
|
97
|
+
`examples/forty_tab_minimal.yaml` is the quickest path. Top-level sections:
|
|
98
|
+
|
|
99
|
+
- `panel_config` — enclosure identity plus `total_tabs`, `main_size`, `postal_code`, `time_zone`, and `islandable`. A grid-forming BESS in an islandable enclosure automatically exposes an integrated MID (the islanding authority), mirroring a real SPAN panel.
|
|
100
|
+
- `circuit_templates` and `circuits` — per-circuit `tabs`, breaker rating, priority, relay behavior, and an optional `device_type` (`evse` or `pv`) to land a DER on a circuit.
|
|
101
|
+
- `bess` — nameplate capacity, charge mode, charge/discharge limits.
|
|
102
|
+
- `ticks` — the per-tick driving signal: signed watts per circuit and the grid-online flag.
|
|
103
|
+
|
|
104
|
+
### 2. DeviceManifest (programmatic)
|
|
105
|
+
|
|
106
|
+
A producer can build `DeviceInstance`s directly instead of using the YAML loader. Each device class's identity and static attributes live in the instance's `metadata`, validated once at startup by `ManifestPhysicsView` (missing required keys or malformed values raise `ManifestValidationError` naming the offending instance). The metadata keys per device class:
|
|
107
|
+
|
|
108
|
+
| entity_class | required keys | optional keys |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `panel` | `vendor-name`, `serial-number`, `firmware-version` (or `software-version`), `hardware-version`, `panel-size`, `main-breaker-rating-a`, `panel-model`, `postal-code`, `time-zone` | `service-voltage-v` (240), `line-voltage-v` (120), `islandable` (false) |
|
|
111
|
+
| `lugs` | `direction` (`upstream` \| `downstream`) | |
|
|
112
|
+
| `circuit` | `tab-numbers` (CSV ints), `breaker-rating-a`, `default-priority`, `relay-behavior`, `placement` (`upstream-of-lugs` \| `downstream-of-lugs`) | `always-on`, `pcs-priority` (0), `initial-consumed-wh` (0), `initial-produced-wh` (0) |
|
|
113
|
+
| `bess` | `vendor-name`, `nameplate-capacity-kwh` | `product-name`, `model`, `serial-number`, `firmware-version`/`software-version`, `relative-position` (`UPSTREAM`), `feed`, `initial-soe-kwh` |
|
|
114
|
+
| `pv` | `vendor-name`, `nameplate-capacity-w`, `inverter-type` (`hybrid` \| `ac-coupled`) | `product-name`, `serial-number`, `firmware-version`/`software-version`, `relative-position` (`IN_PANEL`), `feed` |
|
|
115
|
+
| `evse` | `vendor-name`, `product-name`, `part-number`, `serial-number`, `firmware-version` (or `software-version`), `max-current-a` | `feed` |
|
|
116
|
+
| `mid` | (none) | `vendor-name`, `serial-number`, `product-name`, `model`, `firmware-version`/`software-version`, `hardware-version` |
|
|
117
|
+
|
|
118
|
+
## Usage (as a producer library)
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import time
|
|
122
|
+
|
|
123
|
+
from ebus_panel_sim import (
|
|
124
|
+
BESSConfig, DeviceInstance, DeviceManifest, Emitter,
|
|
125
|
+
LoadSheddingConfig, SetterRegistry, TickInputs,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> None:
|
|
130
|
+
manifest = DeviceManifest(instances=(
|
|
131
|
+
DeviceInstance("panel", "abc-123", "Span Panel", metadata={
|
|
132
|
+
"vendor-name": "Span", "serial-number": "abc-123",
|
|
133
|
+
"firmware-version": "sim/v0.1.0", "hardware-version": "rev2",
|
|
134
|
+
"panel-size": "40", "main-breaker-rating-a": "200",
|
|
135
|
+
"panel-model": "MAIN_40", "postal-code": "94103",
|
|
136
|
+
"time-zone": "America/Los_Angeles", "islandable": "true",
|
|
137
|
+
}),
|
|
138
|
+
DeviceInstance("lugs", "abc-123-lugs-up", "Upstream lugs", {"direction": "upstream"}),
|
|
139
|
+
DeviceInstance("circuit", "kitchen", "Kitchen", metadata={
|
|
140
|
+
"tab-numbers": "1", "breaker-rating-a": "20",
|
|
141
|
+
"default-priority": "NICE_TO_HAVE", "relay-behavior": "controllable",
|
|
142
|
+
"placement": "downstream-of-lugs",
|
|
143
|
+
}),
|
|
144
|
+
DeviceInstance("bess", "abc-123-bess", "Battery", metadata={
|
|
145
|
+
"vendor-name": "Span", "nameplate-capacity-kwh": "13.5",
|
|
146
|
+
}),
|
|
147
|
+
))
|
|
148
|
+
bess_cfg = BESSConfig(instance_id="abc-123-bess", nameplate_capacity_kwh=13.5,
|
|
149
|
+
max_charge_w=3500.0, max_discharge_w=3500.0)
|
|
150
|
+
|
|
151
|
+
# The emitter owns the MQTT connection: ebus-sdk builds the client from
|
|
152
|
+
# mqtt_cfg and sets the enclosure's LWT. Empty SetterRegistry -> the emitter
|
|
153
|
+
# installs internal default /set handlers; register your own before
|
|
154
|
+
# construction to override them.
|
|
155
|
+
emitter = Emitter(
|
|
156
|
+
manifest, SetterRegistry(),
|
|
157
|
+
mqtt_cfg={"host": "127.0.0.1", "port": 1883},
|
|
158
|
+
bess_configs=(bess_cfg,),
|
|
159
|
+
load_shedding_config=LoadSheddingConfig(soc_threshold_pct=20.0),
|
|
160
|
+
)
|
|
161
|
+
emitter.start()
|
|
162
|
+
try:
|
|
163
|
+
while True:
|
|
164
|
+
emitter.publish_tick(TickInputs(
|
|
165
|
+
current_time=time.time(),
|
|
166
|
+
grid_online=True,
|
|
167
|
+
circuits=collect_powers_from_your_model(), # instance_id -> signed watts
|
|
168
|
+
))
|
|
169
|
+
time.sleep(1.0)
|
|
170
|
+
finally:
|
|
171
|
+
emitter.stop()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
main()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Read the most recently published state back through `emitter.last_snapshot`. `mqtt_cfg` is handed straight to ebus-sdk: beyond `host`/`port` it takes the ebus-mqtt-client TLS and authentication keys for secured brokers (e.g. broker-quickstart's mTLS `discovery`/`strict` profiles).
|
|
178
|
+
|
|
179
|
+
## Layout
|
|
180
|
+
|
|
181
|
+
- `src/ebus_panel_sim/` — the package (`emitter.py`, `manifest.py`, `wire/` profiles + publishing, `native_devices/`); see [DESIGN.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DESIGN.md).
|
|
182
|
+
- `examples/` — the runnable example and its YAML definition.
|
|
183
|
+
- `tests/` — the pytest suite.
|
|
184
|
+
|
|
185
|
+
## Tests
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv run pytest
|
|
189
|
+
uv run mypy --strict src/ebus_panel_sim tests
|
|
190
|
+
uv run ruff check src tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
|
|
195
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/CONTRIBUTING.md) for how to file issues, start a [discussion](https://github.com/electrification-bus/distribution-enclosure-simulator/discussions), and open pull requests, plus the local quality gates (ruff, mypy `--strict`, pytest).
|
|
196
|
+
|
|
197
|
+
## Credits
|
|
198
|
+
|
|
199
|
+
A fork of, and building on, the original simulator created by **Bill Flood** ([@cayossarian](https://github.com/cayossarian)); since updated to track the latest eBus specification. See [AUTHORS](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/AUTHORS).
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
See [LICENSE](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/LICENSE).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# ebus-panel-sim
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/ebus-panel-sim/)
|
|
3
4
|
[](https://github.com/electrification-bus/distribution-enclosure-simulator/actions/workflows/ci.yaml)
|
|
4
|
-
[](https://pypi.org/project/ebus-panel-sim/)
|
|
5
6
|
[](https://github.com/astral-sh/ruff)
|
|
6
7
|
[](https://github.com/electrification-bus/specification/tree/6e582c994fff4c77853a79d8bab26ef9924e22c7)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -13,7 +14,7 @@ It serves two roles:
|
|
|
13
14
|
- **Simulator / test fixture.** Drive it from a small YAML definition and it publishes a spec-conformant, fully-commissioned enclosure to any MQTT broker. Consumers (Home Assistant integrations, dashboards, SDK code) validate against it before shipping to the field.
|
|
14
15
|
- **Producer library.** The canonical eBus Homie publisher. A producer (a simulator, a real panel gateway, an LLM-driven model) hands the emitter a small per-tick driving signal (signed power per circuit, current time, grid-online flag) via `TickInputs`; the emitter derives all telemetry and publishes Homie-conformant retained MQTT with diff-only updates. The split is **identity = manifest (once at startup), telemetry = derived from TickInputs (per tick)**.
|
|
15
16
|
|
|
16
|
-
For the internals (the per-tick pipeline, the native BESS/load-shed devices, `/set` handling, the wire model) see [DESIGN.md](DESIGN.md); for the dev setup see [DEVELOPER.md](DEVELOPER.md).
|
|
17
|
+
For the internals (the per-tick pipeline, the native BESS/load-shed devices, `/set` handling, the wire model) see [DESIGN.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DESIGN.md); for the dev setup see [DEVELOPER.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DEVELOPER.md).
|
|
17
18
|
|
|
18
19
|
## Requirements
|
|
19
20
|
|
|
@@ -151,7 +152,7 @@ Read the most recently published state back through `emitter.last_snapshot`. `mq
|
|
|
151
152
|
|
|
152
153
|
## Layout
|
|
153
154
|
|
|
154
|
-
- `src/ebus_panel_sim/` — the package (`emitter.py`, `manifest.py`, `wire/` profiles + publishing, `native_devices/`); see [DESIGN.md](DESIGN.md).
|
|
155
|
+
- `src/ebus_panel_sim/` — the package (`emitter.py`, `manifest.py`, `wire/` profiles + publishing, `native_devices/`); see [DESIGN.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/DESIGN.md).
|
|
155
156
|
- `examples/` — the runnable example and its YAML definition.
|
|
156
157
|
- `tests/` — the pytest suite.
|
|
157
158
|
|
|
@@ -165,12 +166,12 @@ uv run ruff check src tests
|
|
|
165
166
|
|
|
166
167
|
## Contributing
|
|
167
168
|
|
|
168
|
-
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to file issues, start a [discussion](https://github.com/electrification-bus/distribution-enclosure-simulator/discussions), and open pull requests, plus the local quality gates (ruff, mypy `--strict`, pytest).
|
|
169
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/CONTRIBUTING.md) for how to file issues, start a [discussion](https://github.com/electrification-bus/distribution-enclosure-simulator/discussions), and open pull requests, plus the local quality gates (ruff, mypy `--strict`, pytest).
|
|
169
170
|
|
|
170
171
|
## Credits
|
|
171
172
|
|
|
172
|
-
A fork of, and building on, the original simulator created by **Bill Flood** ([@cayossarian](https://github.com/cayossarian)); since updated to track the latest eBus specification. See [AUTHORS](AUTHORS).
|
|
173
|
+
A fork of, and building on, the original simulator created by **Bill Flood** ([@cayossarian](https://github.com/cayossarian)); since updated to track the latest eBus specification. See [AUTHORS](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/AUTHORS).
|
|
173
174
|
|
|
174
175
|
## License
|
|
175
176
|
|
|
176
|
-
See [LICENSE](LICENSE).
|
|
177
|
+
See [LICENSE](https://github.com/electrification-bus/distribution-enclosure-simulator/blob/main/LICENSE).
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# PEP 639 floor: `license = "MIT"` as an SPDX expression plus `license-files` needs
|
|
3
|
+
# hatchling >= 1.27. Below it the build SUCCEEDS and quietly emits the legacy
|
|
4
|
+
# metadata instead (measured on 1.26.3: `License: MIT`, no `License-Expression`,
|
|
5
|
+
# no `License-File`), so an unpinned backend would degrade the published metadata
|
|
6
|
+
# without failing anything.
|
|
7
|
+
requires = ["hatchling>=1.27"]
|
|
3
8
|
build-backend = "hatchling.build"
|
|
4
9
|
|
|
5
10
|
[project]
|
|
@@ -9,11 +14,32 @@ name = "ebus-panel-sim"
|
|
|
9
14
|
# git tag disagrees. Matches the convention across the eBus family.
|
|
10
15
|
dynamic = ["version"]
|
|
11
16
|
description = "Producer-side Homie publisher with embedded behaviour runtime for the eBus convention"
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICENSE"]
|
|
12
20
|
authors = [
|
|
13
21
|
{name = "Bill Flood"},
|
|
14
22
|
{name = "Clark Communications Corporation"},
|
|
15
23
|
]
|
|
16
24
|
keywords = ["homie", "homie5", "mqtt", "iot", "ebus", "simulator", "span", "distribution-enclosure", "energy"]
|
|
25
|
+
# `requires-python` is what pip enforces; these are what PyPI indexes and what the
|
|
26
|
+
# README's Python badge reads. Kept in step with the floor above, not with the CI
|
|
27
|
+
# matrix, which tests the ends of the range (3.11 and 3.14) rather than every point
|
|
28
|
+
# in it.
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 4 - Beta",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
|
+
"Topic :: Home Automation",
|
|
38
|
+
"Topic :: System :: Emulators",
|
|
39
|
+
# The package ships py.typed; this is how PyPI surfaces that, on the page and
|
|
40
|
+
# in the "Typed" search filter.
|
|
41
|
+
"Typing :: Typed",
|
|
42
|
+
]
|
|
17
43
|
requires-python = ">=3.11"
|
|
18
44
|
dependencies = [
|
|
19
45
|
"ebus-sdk>=0.19,<0.20",
|
|
@@ -69,7 +69,7 @@ from ebus_panel_sim.wire.set_router import SetterHandler, SetterRegistry
|
|
|
69
69
|
# via `[tool.hatch.version]`, and publish.yml refuses to release when the git tag
|
|
70
70
|
# disagrees. Bump it in this one place. Note this is the PACKAGE version and is
|
|
71
71
|
# distinct from the producer-contract version the docstrings above refer to.
|
|
72
|
-
__version__ = "0.3.
|
|
72
|
+
__version__ = "0.3.2"
|
|
73
73
|
|
|
74
74
|
__all__ = [
|
|
75
75
|
"BESSConfig",
|
|
@@ -49,7 +49,7 @@ from ebus_panel_sim.snapshot import (
|
|
|
49
49
|
EbusPvSnapshot,
|
|
50
50
|
)
|
|
51
51
|
from ebus_panel_sim.tick_inputs import TickInputs
|
|
52
|
-
from ebus_panel_sim.wire._sdk_seam import owned_client
|
|
52
|
+
from ebus_panel_sim.wire._sdk_seam import owned_client, publish_will_now
|
|
53
53
|
from ebus_panel_sim.wire.bag_builder import BagBuilder
|
|
54
54
|
from ebus_panel_sim.wire.graph_builder import build_graph
|
|
55
55
|
from ebus_panel_sim.wire.mapping_loader import load_mapping_table
|
|
@@ -253,11 +253,27 @@ class Emitter:
|
|
|
253
253
|
Graceful (default): publish the root's ``$state=disconnected`` then stop
|
|
254
254
|
the shared client (ebus-sdk's bounded teardown; per Homie's
|
|
255
255
|
effective-state rule the root going disconnected covers every child).
|
|
256
|
-
|
|
257
|
-
the
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
|
|
257
|
+
Non-graceful: leave the tree looking like a producer that died, by
|
|
258
|
+
publishing the root's ``$state=lost`` retained before dropping the
|
|
259
|
+
connection. This is what a consumer test wants from a simulator, and it
|
|
260
|
+
is emphatically NOT what a bare disconnect gives you: the Last Will fires
|
|
261
|
+
only on an *unclean* disconnect, and every teardown here closes cleanly,
|
|
262
|
+
so relying on the will would leave the whole retained tree claiming
|
|
263
|
+
``ready`` forever. See ``publish_will_now``, which sources the topic and
|
|
264
|
+
payload from ``Device.will()`` so this cannot drift from the real thing.
|
|
265
|
+
|
|
266
|
+
The difference from a real will is timing and delivery, not content: this
|
|
267
|
+
lands immediately over the live connection, where a broker-delivered will
|
|
268
|
+
waits on keepalive expiry. A consumer exercising the *retained* view sees
|
|
269
|
+
the same thing either way; one exercising live will delivery does not.
|
|
270
|
+
|
|
271
|
+
``clear_retained`` additionally clears every device's retained values +
|
|
272
|
+
``$description`` before disconnecting, for a clean-slate re-run. It
|
|
273
|
+
applies to the graceful path only, since a producer that died clears
|
|
274
|
+
nothing."""
|
|
260
275
|
if not graceful:
|
|
276
|
+
publish_will_now(self._root)
|
|
261
277
|
client = owned_client(self._root.mqttc)
|
|
262
278
|
if client is not None:
|
|
263
279
|
client.stop()
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
import ebus_sdk
|
|
14
|
-
from ebus_sdk import MqttClient, PropertyDatatype, Unit
|
|
14
|
+
from ebus_sdk import EBUS_HOMIE_MQTT_QOS, MqttClient, PropertyDatatype, Unit
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def make_property(
|
|
@@ -53,3 +53,36 @@ def owned_client(mqttc: object) -> MqttClient | None:
|
|
|
53
53
|
its owner to stop, which is the behaviour the SDK's contract asks for.
|
|
54
54
|
"""
|
|
55
55
|
return mqttc if isinstance(mqttc, MqttClient) else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def publish_will_now(root: ebus_sdk.Device) -> bool:
|
|
59
|
+
"""Publish the root's will payload (``$state=lost``) as a retained message, now.
|
|
60
|
+
|
|
61
|
+
A Last Will fires only when the broker sees an UNCLEAN disconnect. Every
|
|
62
|
+
orderly teardown path here sends a clean DISCONNECT, and ebus-mqtt-client does
|
|
63
|
+
that deliberately (``MqttClient.stop`` -> ``mqttc.disconnect()``), precisely so
|
|
64
|
+
a normal shutdown is not reported to consumers as a crash. The consequence is
|
|
65
|
+
that a simulator asked to *act* like a producer that died cannot get there by
|
|
66
|
+
letting the will fire: it has to publish the will's own payload itself.
|
|
67
|
+
|
|
68
|
+
Topic and payload come from ``Device.will()``, the same descriptor the SDK
|
|
69
|
+
registers as the LWT, so this cannot drift from what a real will would have
|
|
70
|
+
delivered.
|
|
71
|
+
|
|
72
|
+
QoS deliberately follows the rest of the tree's ``$state`` publishes rather
|
|
73
|
+
than the ``qos=0`` default the will registration uses: a registered will is
|
|
74
|
+
delivered by the broker from its own state, whereas this goes out over a live
|
|
75
|
+
connection that is about to close and has to actually land first.
|
|
76
|
+
|
|
77
|
+
Returns True when the broker acknowledged it, False if there is no owned
|
|
78
|
+
client or the publish did not flush in time.
|
|
79
|
+
"""
|
|
80
|
+
client = owned_client(root.mqttc)
|
|
81
|
+
if client is None:
|
|
82
|
+
return False
|
|
83
|
+
will = root.will()
|
|
84
|
+
return bool(
|
|
85
|
+
client.publish_and_flush(
|
|
86
|
+
will["topic"], will["payload"], qos=EBUS_HOMIE_MQTT_QOS, retain=True
|
|
87
|
+
)
|
|
88
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Teardown semantics: what the retained tree looks like after ``Emitter.stop()``.
|
|
2
|
+
|
|
3
|
+
Both modes were previously untested. The non-graceful path had documented itself as
|
|
4
|
+
"leaving the LWT to fire ``$state=lost``", which cannot happen: a Last Will fires
|
|
5
|
+
only on an *unclean* disconnect, and ``MqttClient.stop()`` sends a clean DISCONNECT
|
|
6
|
+
(deliberately, so an orderly shutdown is not reported as a crash). Verified against
|
|
7
|
+
a real mosquitto broker before this suite existed: a consumer joining after an
|
|
8
|
+
ungraceful teardown read the whole tree as ``ready``.
|
|
9
|
+
|
|
10
|
+
These tests pin the observable outcome, the effective retained ``$state`` a
|
|
11
|
+
late-joining consumer sees, rather than the mechanism.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ebus_panel_sim import Emitter
|
|
17
|
+
from ebus_panel_sim.wire.set_router import SetterRegistry
|
|
18
|
+
|
|
19
|
+
from .conftest import PahoRecorder
|
|
20
|
+
from .test_emitter_public_surface import _manifest
|
|
21
|
+
|
|
22
|
+
ROOT_STATE = "ebus/5/p1/$state"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _started() -> Emitter:
|
|
26
|
+
emitter = Emitter(_manifest(), SetterRegistry())
|
|
27
|
+
emitter.start()
|
|
28
|
+
return emitter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_ungraceful_stop_leaves_the_root_lost(rec: PahoRecorder) -> None:
|
|
32
|
+
"""A producer that died must not leave the tree claiming ``ready``."""
|
|
33
|
+
_started().stop(graceful=False)
|
|
34
|
+
assert rec.retained[ROOT_STATE] == "lost"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_graceful_stop_leaves_the_root_disconnected(rec: PahoRecorder) -> None:
|
|
38
|
+
"""An orderly shutdown is a different signal from a crash, and consumers act
|
|
39
|
+
on the difference: ``disconnected`` is expected, ``lost`` is a fault."""
|
|
40
|
+
_started().stop(graceful=True)
|
|
41
|
+
assert rec.retained[ROOT_STATE] == "disconnected"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_the_two_modes_do_not_agree(rec: PahoRecorder) -> None:
|
|
45
|
+
"""Guards against a future change collapsing the two teardown modes into one.
|
|
46
|
+
|
|
47
|
+
Note this test passed before the fix too, for the wrong reason: ungraceful left
|
|
48
|
+
the root on ``ready``, which does differ from ``disconnected``. The bug was
|
|
49
|
+
never that the modes agreed, it was that one of them lied. The two tests above
|
|
50
|
+
are what pin the actual values; this one only pins that they stay distinct."""
|
|
51
|
+
_started().stop(graceful=False)
|
|
52
|
+
ungraceful = rec.retained[ROOT_STATE]
|
|
53
|
+
rec.reset()
|
|
54
|
+
_started().stop(graceful=True)
|
|
55
|
+
assert ungraceful != rec.retained[ROOT_STATE]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_ungraceful_stop_uses_the_sdk_will_descriptor(rec: PahoRecorder) -> None:
|
|
59
|
+
"""The synthesised ``lost`` must be byte-identical to what the broker would
|
|
60
|
+
have delivered from the registered will, or the simulation is a fiction."""
|
|
61
|
+
emitter = _started()
|
|
62
|
+
will = emitter._root.will()
|
|
63
|
+
emitter.stop(graceful=False)
|
|
64
|
+
assert rec.retained[will["topic"]] == will["payload"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_ungraceful_stop_publishes_lost_retained(rec: PahoRecorder) -> None:
|
|
68
|
+
"""Retained, or a consumer joining after the death sees nothing at all and
|
|
69
|
+
cannot distinguish a dead producer from one that never existed."""
|
|
70
|
+
_started().stop(graceful=False)
|
|
71
|
+
lost = [(t, d, q, r) for (t, d, q, r) in rec.published if t == ROOT_STATE and d == "lost"]
|
|
72
|
+
assert lost, "no $state=lost publish reached the transport"
|
|
73
|
+
assert all(retain for (_t, _d, _q, retain) in lost)
|
ebus_panel_sim-0.3.0/PKG-INFO
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ebus-panel-sim
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Producer-side Homie publisher with embedded behaviour runtime for the eBus convention
|
|
5
|
-
Project-URL: Homepage, https://ebus.energy
|
|
6
|
-
Project-URL: Repository, https://github.com/electrification-bus/distribution-enclosure-simulator
|
|
7
|
-
Project-URL: Issues, https://github.com/electrification-bus/distribution-enclosure-simulator/issues
|
|
8
|
-
Author: Bill Flood, Clark Communications Corporation
|
|
9
|
-
License-File: AUTHORS
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: distribution-enclosure,ebus,energy,homie,homie5,iot,mqtt,simulator,span
|
|
12
|
-
Requires-Python: >=3.11
|
|
13
|
-
Requires-Dist: ebus-sdk<0.20,>=0.19
|
|
14
|
-
Requires-Dist: pyyaml>=6.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/native_devices/load_shedding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/charge-limit.json
RENAMED
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/connection.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/load-shed.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/power-flows.json
RENAMED
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/catalogs/shed-forecast.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/circuit.json
RENAMED
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/evse.json
RENAMED
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/lugs.json
RENAMED
|
File without changes
|
{ebus_panel_sim-0.3.0 → ebus_panel_sim-0.3.2}/src/ebus_panel_sim/wire/profiles/span/panel.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|