span-panel-api 3.0.0b2__tar.gz → 3.0.0b3__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.
- span_panel_api-3.0.0b3/.env.example +54 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.gitignore +6 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.markdownlint-cli2.jsonc +6 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.pre-commit-config.yaml +12 -7
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/CHANGELOG.md +37 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/PKG-INFO +2 -2
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/RELEASE.md +4 -4
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/CHANGELOG.md +16 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/pyproject.toml +1 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/adapter.py +17 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/consumer.py +8 -4
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/field_metadata.py +8 -4
- span_panel_api-3.0.0b3/packages/schema-1/CHANGELOG.md +103 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/pyproject.toml +8 -3
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/breaker.json +52 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/connection.json +72 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/door.json +17 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/grid-forming.json +21 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/grid.json +38 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/info.json +52 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/load-shed.json +18 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/meter.json +201 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/pcs.json +111 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/power-flows.json +35 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/shed.json +24 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/soc.json +35 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/status.json +28 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/switch.json +29 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/fixtures/simulator_tree.json +4984 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/fixtures/simulator_wire.json +679 -0
- span_panel_api-3.0.0b3/packages/schema-1/spec/registries/device-types.md +56 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/adapter.py +61 -10
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/const.py +12 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/devices.py +245 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +56 -3
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/panel.py +223 -5
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +77 -13
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json +53 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/pyproject.toml +11 -2
- span_panel_api-3.0.0b3/scripts/capture_flat_reference.py +130 -0
- span_panel_api-3.0.0b3/scripts/capture_live_flat.py +147 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/verify_adapterless_install.py +4 -5
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/__init__.py +2 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/adapters.py +98 -67
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/factory.py +5 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/models.py +70 -6
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/client.py +382 -13
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/protocol.py +12 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/conftest.py +30 -0
- span_panel_api-3.0.0b3/tests/fixtures/flat_wire.json +563 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_adapters_discovery.py +62 -14
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_auth_and_homie_helpers.py +0 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_detection_auth.py +0 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_factory_dispatch.py +1 -1
- span_panel_api-3.0.0b3/tests/test_live_flat_differential.py +166 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_mqtt_connect_flow.py +65 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_mqtt_homie.py +7 -7
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_protocol_conformance.py +1 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_protocol_models.py +0 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_public_api_unchanged.py +3 -0
- span_panel_api-3.0.0b3/tests/test_redispatch_on_reconnect.py +301 -0
- span_panel_api-3.0.0b3/tests/test_schema_generation_cross_check.py +119 -0
- span_panel_api-3.0.0b3/tests/test_schema_migration_delta.py +475 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_adapter.py +165 -4
- span_panel_api-3.0.0b3/tests/test_schema_one_against_simulator.py +257 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_conformance.py +516 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_devices.py +55 -10
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_panel.py +175 -1
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_snapshot.py +26 -10
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/uv.lock +14 -14
- span_panel_api-3.0.0b2/packages/schema-1/CHANGELOG.md +0 -60
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/devices.py +0 -143
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.codefactor +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.codefactor.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.deps-installed +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/dependabot.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/workflows/ci.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-approve.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.github/workflows/release.yml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.markdownlint.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.prettierrc.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.vscode/extensions.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/.vscode/tasks.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/DEVELOPMENT.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/LICENSE +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/README.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/SECURITY.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/conftest.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/developer_attribute_readme.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/openapi.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/README.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/__init__.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/accumulator.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/const.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/py.typed +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/README.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/__init__.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/circuits.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/py.typed +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/packages/schema-1/src/span_panel_api_schema_1/transport.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/pytest.ini +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/pytest_output.log +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/__init__.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/coverage.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/format.sh +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/format_markdown.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/test_live_auth.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/scripts/verify_reconnect.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/setup-hooks.sh +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/parent_child_tree.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/v2/homie_schema.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_mqtt_client_connection.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_packaging.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_circuits.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_one_transport.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.0.0b2 → span_panel_api-3.0.0b3}/tests/test_schema_zero_adapter.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Local-developer environment variables for span-panel-api.
|
|
2
|
+
#
|
|
3
|
+
# Copy to `.env` and fill in. `.env` is gitignored and must stay that way.
|
|
4
|
+
#
|
|
5
|
+
# `tests/conftest.py` reads this file directly, so no direnv or dotenv package is
|
|
6
|
+
# needed. A value already exported in your shell wins over anything here — the
|
|
7
|
+
# file supplies defaults, it does not override an intentional choice.
|
|
8
|
+
#
|
|
9
|
+
# Everything below is optional. Without it the suite runs in full and the checks
|
|
10
|
+
# that need a sibling checkout skip themselves rather than fail, which is what CI
|
|
11
|
+
# does. They are the *provenance* half of the schema_1 conformance suite: they
|
|
12
|
+
# verify that the vendored copies still match their sources. The conformance and
|
|
13
|
+
# coverage checks, which are the ones that catch real defects, run regardless.
|
|
14
|
+
|
|
15
|
+
# A checkout of the eBus specification.
|
|
16
|
+
#
|
|
17
|
+
# git clone https://github.com/electrification-bus/specification
|
|
18
|
+
#
|
|
19
|
+
# Enables the byte comparison of `packages/schema-1/spec/catalogs/*.json` against
|
|
20
|
+
# the specification's `capabilities/`. Position the checkout at the commit
|
|
21
|
+
# `spec_lock.json` pins (`synced_commit`) before believing a failure — a checkout
|
|
22
|
+
# on a newer HEAD reports differences that are drift, not corruption.
|
|
23
|
+
#EBUS_SPEC_DIR=/path/to/specification
|
|
24
|
+
|
|
25
|
+
# A checkout of SpanPanel/panelbench, the publisher this parser is developed
|
|
26
|
+
# against.
|
|
27
|
+
#
|
|
28
|
+
# git clone git@github.com:SpanPanel/panelbench.git
|
|
29
|
+
#
|
|
30
|
+
# Enables verifying the two vendored captures and the recorded peer pins against
|
|
31
|
+
# the producer itself. The tree capture is compared byte for byte; the wire
|
|
32
|
+
# capture is compared on shape, because its values are perturbed by the
|
|
33
|
+
# simulator's `noise_factor` and an advancing clock.
|
|
34
|
+
#PANELBENCH_DIR=/path/to/panelbench
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# A live SPAN panel running flat firmware (optional, and nothing needs it)
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
#
|
|
40
|
+
# Enables `scripts/capture_live_flat.py`, which takes a retained capture from a
|
|
41
|
+
# real panel so the frozen flat simulator can be measured against firmware rather
|
|
42
|
+
# than trusted. Without it, `test_live_flat_differential.py` skips.
|
|
43
|
+
#
|
|
44
|
+
# The username IS the panel serial, so treat both of these as secrets and keep
|
|
45
|
+
# them here. The capture the script writes is gitignored for the same reason: it
|
|
46
|
+
# carries the serial, the household's circuit names and real consumption. Only the
|
|
47
|
+
# differential's verdict is ever committed.
|
|
48
|
+
#
|
|
49
|
+
# TLS is on and certificate validation is off: the panel presents a self-signed
|
|
50
|
+
# certificate.
|
|
51
|
+
#LIVE_PANEL_HOST=192.168.1.50
|
|
52
|
+
#LIVE_PANEL_PORT=8883
|
|
53
|
+
#LIVE_PANEL_USERNAME=your-panel-serial
|
|
54
|
+
#LIVE_PANEL_PASSWORD=
|
|
@@ -34,3 +34,9 @@ dmypy.json
|
|
|
34
34
|
coverage_output.log
|
|
35
35
|
**/.DS_Store
|
|
36
36
|
.local_coverage_data
|
|
37
|
+
|
|
38
|
+
# Captures taken from a real panel. These carry the panel's serial (which is also
|
|
39
|
+
# its MQTT username), the household's circuit names, and real consumption — none
|
|
40
|
+
# of which belongs in a repository. The differential that reads them commits its
|
|
41
|
+
# *verdict* only, never the capture, and skips when the file is absent.
|
|
42
|
+
tests/fixtures/live_*.json
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
},
|
|
37
37
|
"globs": ["**/*.md"],
|
|
38
38
|
"ignores": [
|
|
39
|
+
// Byte copies of the eBus specification, verified by byte comparison in
|
|
40
|
+
// tests/test_schema_one_conformance.py. Upstream's line lengths are not
|
|
41
|
+
// ours to correct, and a fix here would invalidate that comparison.
|
|
42
|
+
// `globs` above scans the tree directly, so pre-commit's `exclude` cannot
|
|
43
|
+
// filter this out -- it has to be ignored here.
|
|
44
|
+
"packages/schema-1/spec/**",
|
|
39
45
|
".venv/**",
|
|
40
46
|
"venv/**",
|
|
41
47
|
"node_modules/**",
|
|
@@ -3,10 +3,15 @@ repos:
|
|
|
3
3
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
4
4
|
rev: v5.0.0
|
|
5
5
|
hooks:
|
|
6
|
+
# `packages/schema-1/spec/` holds byte copies of the eBus specification,
|
|
7
|
+
# verified by byte comparison in tests/test_schema_one_conformance.py. Any
|
|
8
|
+
# hook that rewrites a file must skip it: a "fix" there would silently
|
|
9
|
+
# invalidate the comparison that makes the copies trustworthy. Non-mutating
|
|
10
|
+
# checks (check-json) deliberately still run, since a corrupt copy should fail.
|
|
6
11
|
- id: trailing-whitespace
|
|
7
|
-
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
12
|
+
exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
8
13
|
- id: end-of-file-fixer
|
|
9
|
-
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
14
|
+
exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
10
15
|
- id: check-yaml
|
|
11
16
|
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
12
17
|
- id: check-toml
|
|
@@ -20,7 +25,7 @@ repos:
|
|
|
20
25
|
exclude: '^src/span_panel_api/generated_client/.*|generate_client\.py|scripts/.*|tests/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*'
|
|
21
26
|
- id: mixed-line-ending
|
|
22
27
|
args: ['--fix=lf']
|
|
23
|
-
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
28
|
+
exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*'
|
|
24
29
|
|
|
25
30
|
# Ruff for formatting and linting
|
|
26
31
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
@@ -55,7 +60,7 @@ repos:
|
|
|
55
60
|
- id: prettier
|
|
56
61
|
types: [markdown]
|
|
57
62
|
args: ['--config', '.prettierrc.json']
|
|
58
|
-
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*'
|
|
63
|
+
exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*'
|
|
59
64
|
|
|
60
65
|
# Markdownlint for markdown files (after Prettier formatting)
|
|
61
66
|
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
|
@@ -63,7 +68,7 @@ repos:
|
|
|
63
68
|
hooks:
|
|
64
69
|
- id: markdownlint-cli2
|
|
65
70
|
args: ['--config', '.markdownlint-cli2.jsonc']
|
|
66
|
-
exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*'
|
|
71
|
+
exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*'
|
|
67
72
|
|
|
68
73
|
# MyPy for type checking
|
|
69
74
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
@@ -80,7 +85,7 @@ repos:
|
|
|
80
85
|
# schema-1 parses the parent/child tree with the eBus SDK, which
|
|
81
86
|
# ships py.typed — so the hook needs it installed to resolve those
|
|
82
87
|
# types rather than silently reporting import-not-found.
|
|
83
|
-
- ebus-sdk>=0.
|
|
88
|
+
- ebus-sdk>=0.19.0
|
|
84
89
|
args: ['--config-file=pyproject.toml']
|
|
85
90
|
exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/.*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*'
|
|
86
91
|
|
|
@@ -98,7 +103,7 @@ repos:
|
|
|
98
103
|
- paho-mqtt
|
|
99
104
|
# schema-1 imports the eBus SDK; without it here the hook reports
|
|
100
105
|
# import-error for a dependency that is correctly declared.
|
|
101
|
-
- ebus-sdk>=0.
|
|
106
|
+
- ebus-sdk>=0.19.0
|
|
102
107
|
exclude: '^src/span_panel_api/generated_client/.*|tests/.*|generate_client\.py|scripts/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*'
|
|
103
108
|
|
|
104
109
|
# Check for common security issues
|
|
@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.0.0b3] - 08/2026
|
|
8
|
+
|
|
9
|
+
Pre-release. Normalises DER identity onto v1.0's vocabulary, and stops deriving the grid answers that v1.0 states outright.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **BREAKING — DER identity speaks v1.0's vocabulary on every device class.** `model` is the human designation and `part_number` the SKU, on `battery`, `evse` and `pv` alike. `product_name` is retired on all three. Flat is the inconsistent side, not v1.0:
|
|
14
|
+
it puts the SKU in `bess/model` and in `evse/part-number`, the same concept under two names, and gives PV neither. `schema_1` used to cross over (`info/part-number` → `battery.model`) to hold each entity's displayed meaning still, which worked and
|
|
15
|
+
permanently encoded flat's irregularity in the snapshot. `schema_0` now translates flat into the normalised shape instead of mirroring it. Measured: every EVSE identity field reads identically on both adapters, so for that device class identity stops
|
|
16
|
+
being a migration delta at all. **`battery.model` changes value for existing flat users at this upgrade** — it gains the designation where it carried the SKU. That is the deliberate trade: a change we schedule in a library release beats the same change
|
|
17
|
+
arriving unplanned during a firmware upgrade a user did not choose the timing of.
|
|
18
|
+
- **Consumers reading `product_name` must move to `model` in the same release.** The Home Assistant integration builds its device-registry model from it; left unchanged, device cards go blank.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`SpanMidSnapshot`, and `SpanPanelSnapshot.mid`.** v1.0 publishes a Microgrid Interconnect Device and the enclosure model puts the `grid` capability on it rather than on the enclosure, so islanding state, grid state and the grid-forming entity live
|
|
23
|
+
there. Previously one of its five properties was read and the device discarded. Purely additive: no flat panel publishes a MID, so nothing existing changes. Presence is `snapshot.mid is not None` rather than a sentinel field, and identity is
|
|
24
|
+
`info/serial-number` rather than the Homie device id, which the proxy model warns is not stable across a proxy-to-native transition.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Adapter discovery no longer blocks the caller's event loop, and no longer imports adapters the panel will never use.** Two defects with one cause: discovery resolved the whole entry-point group up front, on the calling thread. A flat panel therefore
|
|
29
|
+
imported `schema_1` — and with it the eBus SDK and jsonschema — on every connection, for a parser it would not call. Home Assistant reported the whole sequence (`listdir`, `read_text`, `open`, `scandir`) as blocking calls inside the event loop and asked
|
|
30
|
+
for a bug report, with setup stalled 2.0s on a cold import cache. Enumeration and resolution are now separate: `installed_adapter_keys()` reads distribution metadata only, and an adapter is imported the first time a panel asks for that key. The async
|
|
31
|
+
paths run both in a thread. Resolution stays cached per key, which is what keeps the synchronous pre-rebuild callback free of I/O. **`discover_adapters()` is replaced by `installed_adapter_keys()`**, which returns registered names rather than a registry
|
|
32
|
+
of loaded classes — verifying every name would mean importing every package, which is the cost being removed. `SpanMqttClient.available_adapters` becomes `installed_adapters` for the same reason.
|
|
33
|
+
- **A firmware upgrade to a schema generation this install cannot parse is reported instead of raised into a background task.** The redispatch path resolves the new adapter before touching any state, so a flat-only install that meets a v1.0 panel logs
|
|
34
|
+
which package is missing and keeps the parser it has. Previously `SpanPanelAdapterMissingError` escaped a fire-and-forget task as a bare traceback.
|
|
35
|
+
- **`dsm_state` and `current_run_config` are read from the MID instead of reading `UNKNOWN`.** Both are existing entities that had degraded on v1.0 — not because a source vanished, but because `schema_0` _derives_ them and the derivation was never ported.
|
|
36
|
+
v1.0 states the answer, so the multi-signal heuristic is gone: sensed from a ready MID, falling back to the user's `shed/asserted-islanding-state` when it is not ready, then to a `power-flows/grid` heuristic when there is no MID at all, and unknown
|
|
37
|
+
otherwise. A missing MID never reports on-grid — it means SPAN is not the islanding authority, not that the site is on grid, and a generator-fed island is the counterexample. `PANEL_BACKUP` versus `PANEL_OFF_GRID` becomes authoritative rather than
|
|
38
|
+
guessed, because v1.0 names the forming device and its class is recoverable from the tree.
|
|
39
|
+
- **`grid_islandable` is mapped to `grid-forming/capable`** over the BESS's inverter children, as the disjunction — a panel does not island, its DER does, and flat expressed a property of the DER as a property of the enclosure. It returns `None` rather
|
|
40
|
+
than `False` when nothing publishes it, so absence stays a gap instead of becoming a claim. No producer publishes it today, which is recorded rather than worked around.
|
|
41
|
+
- **EVSE identity survives the migration.** The snapshot key and `node_id` — which a consumer builds a `unique_id` and a device-registry identifier from — were the v1.0 device id on `schema_1` and firmware's node name on `schema_0`, so every charger would
|
|
42
|
+
have orphaned and reappeared as a duplicate. Both are the Drive's serial now, which is what real flat firmware keys by.
|
|
43
|
+
|
|
7
44
|
## [3.0.0b2] - 08/2026
|
|
8
45
|
|
|
9
46
|
Pre-release. Releases the reshaped `SchemaAdapter` protocol that `3.0.0b1` predates, and makes the mismatch between the two detectable rather than fatal at construction.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b3
|
|
4
4
|
Summary: A client library for SPAN Panel API
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
6
|
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
@@ -162,9 +162,9 @@ CI going green proves the build, not the install. The seam this repository is bu
|
|
|
162
162
|
# 1. The bootstrap alone must fail by name, not with ModuleNotFoundError
|
|
163
163
|
python3 -m venv .solo && ./.solo/bin/pip install --pre span-panel-api
|
|
164
164
|
./.solo/bin/python -c "
|
|
165
|
-
from span_panel_api.adapters import
|
|
165
|
+
from span_panel_api.adapters import installed_adapter_keys, resolve_adapter, DEFAULT_ADAPTER_KEY
|
|
166
166
|
from span_panel_api.exceptions import SpanPanelAdapterMissingError
|
|
167
|
-
print('adapters:',
|
|
167
|
+
print('adapters:', installed_adapter_keys())
|
|
168
168
|
try:
|
|
169
169
|
resolve_adapter(DEFAULT_ADAPTER_KEY, 'release check')
|
|
170
170
|
except SpanPanelAdapterMissingError as exc:
|
|
@@ -174,8 +174,8 @@ except SpanPanelAdapterMissingError as exc:
|
|
|
174
174
|
# 2. Both packages: the adapter resolves through discovery
|
|
175
175
|
python3 -m venv .both && ./.both/bin/pip install --pre span-panel-api span-panel-api-schema-0
|
|
176
176
|
./.both/bin/python -c "
|
|
177
|
-
from span_panel_api.adapters import
|
|
178
|
-
print('adapters:',
|
|
177
|
+
from span_panel_api.adapters import installed_adapter_keys
|
|
178
|
+
print('adapters:', installed_adapter_keys())
|
|
179
179
|
"
|
|
180
180
|
```
|
|
181
181
|
|
|
@@ -7,6 +7,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
7
7
|
Note that this package versions on the **library-API axis**, not the wire-format axis. The wire format it parses is fixed — the flat single-device schema, SPAN firmware `r202603` through `r202627` — and is identified by `SUPPORTS_DATA_MODEL_VERSIONS`
|
|
8
8
|
rather than by this version number. A release here means this parser changed, never that the panel did.
|
|
9
9
|
|
|
10
|
+
## [1.0.0b3] - 08/2026
|
|
11
|
+
|
|
12
|
+
Pre-release. Requires `span-panel-api` 3.0.0b2 or newer — unchanged, because nothing added here reaches for anything newer.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **BREAKING — DER identity is translated into v1.0's vocabulary rather than mirroring flat's names.** `model` is the human designation and `part_number` the SKU, on `battery`, `evse` and `pv` alike; `product_name` is retired on all three. Flat is the
|
|
17
|
+
irregular side: it puts the SKU in `bess/model` and in `evse/part-number` — the same concept under two names — and gives PV neither. `schema_1` used to cross over to preserve each entity's displayed meaning, which worked and permanently encoded flat's
|
|
18
|
+
irregularity in the snapshot. This adapter now normalises instead: `bess/model` → `part_number`, `bess/product-name` → `model`. **`battery.model` changes value for existing flat users at this upgrade.** Measured: every EVSE identity field now reads
|
|
19
|
+
identically on both adapters, so for that device class identity stops being a migration delta at all.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`dominant_power_source_payload`.** Flat already speaks this vocabulary, so the value passes through — the method exists because `schema_1` must translate, and a caller should not have to know which schema is underneath. Validated rather than passed
|
|
24
|
+
blindly: an unrecognised value returns `None` and the transport refuses the command, matching `schema_1` rather than putting a string outside the enum on the wire.
|
|
25
|
+
|
|
10
26
|
## [1.0.0b2] - 08/2026
|
|
11
27
|
|
|
12
28
|
Pre-release. Follows the reshaped `SchemaAdapter` protocol released in `span-panel-api` 3.0.0b2.
|
|
@@ -74,5 +74,22 @@ class SchemaZeroAdapter:
|
|
|
74
74
|
return None
|
|
75
75
|
return PROPERTY_SET_TOPIC_FMT.format(serial=self._serial_number, node=core_node, prop="dominant-power-source")
|
|
76
76
|
|
|
77
|
+
def dominant_power_source_payload(self, value: str) -> str | None:
|
|
78
|
+
"""Flat speaks this vocabulary already, so the caller's value passes through.
|
|
79
|
+
|
|
80
|
+
The method exists because `schema_1` has to translate — its successor
|
|
81
|
+
property accepts `NONE`/`ON_GRID`/`OFF_GRID`, not a source class — and a
|
|
82
|
+
caller should not have to know which schema it is talking to. Here the
|
|
83
|
+
translation is the identity.
|
|
84
|
+
|
|
85
|
+
Validated rather than passed blindly: an unrecognised value returns None
|
|
86
|
+
and the transport refuses the command, which matches `schema_1`'s
|
|
87
|
+
behaviour and is better than putting a string outside the enum on the
|
|
88
|
+
wire.
|
|
89
|
+
"""
|
|
90
|
+
allowed = {"GRID", "BATTERY", "PV", "GENERATOR", "NONE", "UNKNOWN"}
|
|
91
|
+
candidate = value.strip().upper()
|
|
92
|
+
return candidate if candidate in allowed else None
|
|
93
|
+
|
|
77
94
|
def register_property_callback(self, callback: Callable[[str, str, str, str | None], None]) -> Callable[[], None]:
|
|
78
95
|
return self._consumer.register_property_callback(callback)
|
|
@@ -322,8 +322,12 @@ class HomieDeviceConsumer:
|
|
|
322
322
|
soe_percentage=_parse_float(soc_str) if soc_str else None,
|
|
323
323
|
soe_kwh=_parse_float(soe_str) if soe_str else None,
|
|
324
324
|
vendor_name=vn if vn else None,
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
# Flat is the irregular side: it puts the SKU in `model` on the BESS and in
|
|
326
|
+
# `part-number` on the EVSE, for the same concept. The snapshot speaks v1.0's
|
|
327
|
+
# vocabulary now, so translate rather than mirror -- `product-name` is the
|
|
328
|
+
# designation and flat's `bess/model` is the SKU.
|
|
329
|
+
model=pn if pn else None,
|
|
330
|
+
part_number=mdl if mdl else None,
|
|
327
331
|
serial_number=sn if sn else None,
|
|
328
332
|
software_version=sw if sw else None,
|
|
329
333
|
nameplate_capacity_kwh=_parse_float(nc) if nc else None,
|
|
@@ -344,7 +348,7 @@ class HomieDeviceConsumer:
|
|
|
344
348
|
|
|
345
349
|
return SpanPVSnapshot(
|
|
346
350
|
vendor_name=vn if vn else None,
|
|
347
|
-
|
|
351
|
+
model=pn if pn else None,
|
|
348
352
|
nameplate_capacity_w=_parse_float(nc) if nc else None,
|
|
349
353
|
feed_circuit_id=normalize_circuit_id(feed) if feed else None,
|
|
350
354
|
relative_position=rel_pos.upper() if rel_pos else None,
|
|
@@ -367,7 +371,7 @@ class HomieDeviceConsumer:
|
|
|
367
371
|
lock_state=self._acc.get_prop(node_id, "lock-state") or "UNKNOWN",
|
|
368
372
|
advertised_current_a=_parse_float(adv) if adv else None,
|
|
369
373
|
vendor_name=self._acc.get_prop(node_id, "vendor-name") or None,
|
|
370
|
-
|
|
374
|
+
model=self._acc.get_prop(node_id, "product-name") or None,
|
|
371
375
|
part_number=self._acc.get_prop(node_id, "part-number") or None,
|
|
372
376
|
serial_number=self._acc.get_prop(node_id, "serial-number") or None,
|
|
373
377
|
software_version=self._acc.get_prop(node_id, "software-version") or None,
|
|
@@ -82,8 +82,12 @@ _PROPERTY_FIELD_MAP: tuple[tuple[str, str, str], ...] = (
|
|
|
82
82
|
(TYPE_BESS, "soc", "battery.soe_percentage"),
|
|
83
83
|
(TYPE_BESS, "soe", "battery.soe_kwh"),
|
|
84
84
|
(TYPE_BESS, "vendor-name", "battery.vendor_name"),
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
# Flat's irregularity, translated rather than mirrored: it puts the designation in
|
|
86
|
+
# `product-name` and the SKU in `model` on the BESS, where the EVSE puts the SKU in
|
|
87
|
+
# `part-number`. The snapshot speaks v1.0's vocabulary, so both land on the field
|
|
88
|
+
# that matches the concept.
|
|
89
|
+
(TYPE_BESS, "product-name", "battery.model"),
|
|
90
|
+
(TYPE_BESS, "model", "battery.part_number"),
|
|
87
91
|
(TYPE_BESS, "serial-number", "battery.serial_number"),
|
|
88
92
|
(TYPE_BESS, "software-version", "battery.software_version"),
|
|
89
93
|
(TYPE_BESS, "nameplate-capacity", "battery.nameplate_capacity_kwh"),
|
|
@@ -91,7 +95,7 @@ _PROPERTY_FIELD_MAP: tuple[tuple[str, str, str], ...] = (
|
|
|
91
95
|
(TYPE_BESS, "grid-state", "panel.grid_state"),
|
|
92
96
|
# --- PV → pv.* -----------------------------------------------------------
|
|
93
97
|
(TYPE_PV, "vendor-name", "pv.vendor_name"),
|
|
94
|
-
(TYPE_PV, "product-name", "pv.
|
|
98
|
+
(TYPE_PV, "product-name", "pv.model"),
|
|
95
99
|
(TYPE_PV, "nameplate-capacity", "pv.nameplate_capacity_w"),
|
|
96
100
|
(TYPE_PV, "feed", "pv.feed_circuit_id"),
|
|
97
101
|
(TYPE_PV, "relative-position", "pv.relative_position"), # IN_PANEL | UPSTREAM | DOWNSTREAM
|
|
@@ -100,7 +104,7 @@ _PROPERTY_FIELD_MAP: tuple[tuple[str, str, str], ...] = (
|
|
|
100
104
|
(TYPE_EVSE, "lock-state", "evse.lock_state"),
|
|
101
105
|
(TYPE_EVSE, "advertised-current", "evse.advertised_current_a"),
|
|
102
106
|
(TYPE_EVSE, "vendor-name", "evse.vendor_name"),
|
|
103
|
-
(TYPE_EVSE, "product-name", "evse.
|
|
107
|
+
(TYPE_EVSE, "product-name", "evse.model"),
|
|
104
108
|
(TYPE_EVSE, "part-number", "evse.part_number"),
|
|
105
109
|
(TYPE_EVSE, "serial-number", "evse.serial_number"),
|
|
106
110
|
(TYPE_EVSE, "software-version", "evse.software_version"),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `span-panel-api-schema-1` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
Note that this package versions on the **library-API axis**, not the wire-format axis. The wire format it parses is the parent/child device tree SPAN firmware `r202633+` publishes, identified by `SUPPORTS_DATA_MODEL_VERSIONS` rather than by this version
|
|
8
|
+
number. A release here means this parser changed, never that the panel did.
|
|
9
|
+
|
|
10
|
+
## [0.1.0b3] - 08/2026
|
|
11
|
+
|
|
12
|
+
Pre-release. **Requires `span-panel-api` 3.0.0b3 or newer** — see Fixed.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Spec conformance checking.** `spec_lock.json` ships with the package and records what this parser targets: the firmware range, the eBus specification commit its vocabulary was read from, and the version of every capability, device and registry it
|
|
17
|
+
implements. It is the consumer counterpart to the simulator's publisher lockfile, and both are pinned to the same specification commit — though the anchor shared between them is the **firmware range**, not that commit, because the specification says what
|
|
18
|
+
a device class _may_ publish while a panel publishes one specific tree.
|
|
19
|
+
- **The 13 capability catalogs this adapter addresses**, byte-copied under `spec/` along with the device-types registry. Vendored rather than depended on because the specification is a git repository of versioned documents, not a package. They exist to be
|
|
20
|
+
checked against, never parsed in production: units and datatypes still come from each device's `$description`, since the catalog is the superset across all hardware rather than a statement about the panel in front of us. Formatting hooks are excluded
|
|
21
|
+
from `spec/`, because a lint fix there would quietly invalidate the byte comparison that makes the copies worth having.
|
|
22
|
+
- **`tests/test_schema_one_conformance.py`**, which asks the consumer's question rather than the publisher's. A publisher asks whether everything it emits is legal, and for it an omission is unremarkable. This asks whether every name the adapter _reads_ is
|
|
23
|
+
one the specification defines — because a consumer addressing a name that no longer exists does not fail, it goes quiet: the property never arrives, metadata lookup returns `None`, and an entity disappears. `ebus-sdk` 0.18.0 removing the `battery`
|
|
24
|
+
capability key in favour of `soc`, with no alias, is exactly that shape.
|
|
25
|
+
- **An explicit SPAN extension allowlist.** Fourteen of the forty-two properties this adapter reads are absent from every catalog — per-phase meter readings, panel link states, circuit `spaces`, the EVSE surface. All are legal, since the specification
|
|
26
|
+
permits properties it has never heard of. They are enumerated with reasons so that a name missing from the catalog must be a deliberate claim about SPAN's vocabulary rather than an unnoticed typo; at runtime the two are indistinguishable. Tests also fail
|
|
27
|
+
when an extension is later adopted upstream, or when one is declared for a property nothing reads.
|
|
28
|
+
- **A peer record and simulator coverage check.** `spec_lock.json` now records the producer this parser is developed against — the SPAN simulator, `role: publisher` — with the specification commit and firmware range it pins, and a captured copy of the tree
|
|
29
|
+
it publishes is vendored alongside the catalogs. Two sides reading different vocabularies is now a test failure rather than something noticed later, and the anchor asserted between them is the **firmware range**, since the specification says what a
|
|
30
|
+
device class may publish while a panel publishes one specific tree.
|
|
31
|
+
- **An explicit record of what the producer does not exercise.** Of the 42 `(capability, property)` pairs this adapter reads, the simulator's captured tree declares 41. The exception is `grid/islanding-state`: the simulator models a MID but its tracked
|
|
32
|
+
config publishes none, so `grid_state` — corrected in `0.1.0b2` to read `islanding-state` rather than `grid-state` — is the single mapping the producer gives no evidence for. Recorded rather than left implicit, because a passing suite otherwise reads as
|
|
33
|
+
coverage it does not have. The entry is rejected once the simulator starts publishing it.
|
|
34
|
+
- **The parser is now driven end to end from what the producer actually publishes.** Every other test in this package runs on a fixture captured off the upstream _generic_ eBus panel simulator, which by construction never carries SPAN's own vocabulary.
|
|
35
|
+
`spec/fixtures/simulator_wire.json` is a capture from SPAN's publisher instead — descriptions, `$state` and all 494 property values across 37 devices — fed in sorted topic order, the way a retained store replays it rather than the way a tree is walked.
|
|
36
|
+
The parser reaches ready on it, sizes the panel from `MAIN_40`, and parses all 30 circuits. Values are deliberately not asserted: the producer's config carries `noise_factor` and its clock advances, so pinning a wattage would fail on every recapture for
|
|
37
|
+
a reason nobody could act on.
|
|
38
|
+
- **Two producer-side gaps are pinned rather than left to be noticed.** `grid_state` stays `None` because nothing instantiates a MID, and every DER — BESS, PV and both EVSEs — declares `info/model` in its `$description` and never publishes a value (PV
|
|
39
|
+
declares five `info` properties and publishes one). The second breaks the single standing obligation eBus places on a publisher, to declare accurately what it publishes, and is invisible to a conformance checker: comparing declarations against catalogs
|
|
40
|
+
cannot see a declaration nothing fulfils. Only a capture carrying values can, which is the argument for this fixture existing. Both are asserted as current expectations, so closing either fails the test that describes it.
|
|
41
|
+
|
|
42
|
+
Provenance (byte comparison against a specification or simulator checkout) is skipped unless `EBUS_SPEC_DIR` / `PANELBENCH_DIR` are set, so conformance and coverage run everywhere while the byte checks stay opportunistic. The wire capture is compared on
|
|
43
|
+
shape rather than bytes for the same reason its values are not asserted. Provenance proves the right bytes were copied; it cannot prove they were understood, which is what the other two are for.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **The conformance check was reading the wrong set of names.** Built from `_PROPERTY_FIELD_MAP` alone, it covered only properties that carry field metadata and silently skipped everything the snapshot mapper reads directly — the MID, `connection`
|
|
48
|
+
feeds/fed-by, `info/direction`. `grid_state`, the most recently corrected mapping in this package, was among them. The read set is now derived from the source itself, so it cannot fall behind the code; that immediately surfaced `info/direction` as a
|
|
49
|
+
fifteenth undeclared extension.
|
|
50
|
+
- **The bootstrap floor is raised to 3.0.0b3**, which is where it should always have been: this parser imports `SpanMidSnapshot`, and 3.0.0b2 does not define it. The declared `>=3.0.0b2` let a resolver pair this wheel with 3.0.0b2 and fail on import.
|
|
51
|
+
Caught before the first release that would have shipped it. `schema-0` keeps its b2 floor; every name it imports is present there, checked rather than assumed.
|
|
52
|
+
|
|
53
|
+
## [0.1.0b2] - 08/2026
|
|
54
|
+
|
|
55
|
+
Pre-release. Corrects the dependency floor `0.1.0b1` shipped with, and follows the reshaped `SchemaAdapter` protocol released in `span-panel-api` 3.0.0b2.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`ADAPTER_CONTRACT = 1`**, declaring which version of the bootstrap-to-adapter contract this parser was built against. Declared as a literal rather than imported from `span_panel_api.protocol`: a value read from the installed bootstrap would agree with
|
|
60
|
+
every bootstrap, which is exactly the disagreement the check exists to find.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **The `span-panel-api` floor was `>=3.0.0b1`, which no published bootstrap could satisfy in practice.** `0.1.0b1` was built against a bootstrap that reads the panel's `data-model-version` and constructs adapters with the whole schema; the only bootstrap
|
|
65
|
+
on PyPI at the time did neither. Its `V2HomieSchema` had no `data_model_version` field at all, so a `1.x` panel could not even be represented, and its factory hardcoded the version to `None` — meaning this adapter was discoverable and never selectable.
|
|
66
|
+
The floor is now `>=3.0.0b2`, the first release where both hold. Nothing was installed against the old floor; the combination was unreachable rather than broken in the field.
|
|
67
|
+
|
|
68
|
+
## [0.1.0b1] - 08/2026
|
|
69
|
+
|
|
70
|
+
Pre-release. First release as a standalone distribution, and the first parser for the parent/child data model.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`SchemaOneAdapter`**, registered as `schema_1` under the `span_panel_api.schema_adapters` entry-point group. A panel reporting `data-model-version` `1.x` resolves to it; a panel without this package installed still gets the named
|
|
75
|
+
`SpanPanelAdapterMissingError`, so installing it is the opt-in.
|
|
76
|
+
- **`ControllerRoutes`** — an `ebus_sdk.MqttControllerTransport` that records `Controller`'s subscriptions instead of making them, so the SDK parses the tree over span-panel-api's own connection to the panel's broker. The adapter is built before a
|
|
77
|
+
connection exists and never receives one; a single wildcard subscription made by the transport layer covers the whole tree, and this routes each message to whichever SDK callback asked for it.
|
|
78
|
+
- **The snapshot mapper.** Sorts the tree by declared device type — never by device id — and maps it onto `SpanPanelSnapshot`: circuits, both lugs, the MID, and the BESS/PV/EVSE devices.
|
|
79
|
+
- **Panel size from `info/model`** via `PANEL_SIZE_BY_MODEL`, which is what restores the unmapped-position entries the integration builds from the difference between total and occupied spaces. `info/spaces` has no format and the panel publishes no size
|
|
80
|
+
property, so the model is the only source; `panel_model_drift()` reports a model the panel declares that we have no size for, because the alternative is a user noticing missing positions.
|
|
81
|
+
- **Field metadata read from each device's `$description`** rather than a schema document. The same capability type exposes different properties on different device classes — `meter` is voltage on the panel, power and energy on a circuit, both currents on
|
|
82
|
+
lugs — so the per-device description is what this panel actually has.
|
|
83
|
+
- **A `py.typed` marker**, so consumers type-check against this package's real annotations.
|
|
84
|
+
|
|
85
|
+
### Known deviations and deliberate gaps
|
|
86
|
+
|
|
87
|
+
- **`set_dominant_power_source_topic()` returns `None`.** The v1.0 property split into `grid-forming-entity` and `asserted-islanding-state`, which are different controls on different devices rather than a rename. `None` makes the transport reject the
|
|
88
|
+
command instead of publishing where nothing listens; which successor to expose is a product decision.
|
|
89
|
+
- **`dsm_state`, `current_run_config`, `grid_islandable` and `pv.relative_position`** have no direct v1.0 equivalent and are left to the product decisions tracked separately. Fields the mapper declines carry no metadata row, so the integration never
|
|
90
|
+
validates against a field nothing populates.
|
|
91
|
+
|
|
92
|
+
### Fixed before first release
|
|
93
|
+
|
|
94
|
+
Both found by verifying reconnect against a live broker, and both presented as a healthy connection.
|
|
95
|
+
|
|
96
|
+
- **Messages arriving before the SDK registered a route for them were dropped.** `Controller` learns its topics as it walks the tree, but one subscription delivers the whole tree at once in whatever order the broker replays its retained store. Seeded
|
|
97
|
+
children-first, a 40-space panel parsed as zero circuits. Unrouted messages are now held and released when the matching route appears — the value a per-device subscription would have been given at subscribe time — with a ceiling so an unclaimed subtree
|
|
98
|
+
cannot leak.
|
|
99
|
+
- **Readiness asked only about the root**, so a connection completed with a fraction of its circuits and no panel size. It now waits for every declared device to describe itself, at any depth. Child _state_ is deliberately not required, so an offline DER
|
|
100
|
+
does not block a connection; the model is required only when the root's description declares it.
|
|
101
|
+
- **`grid_state` read the wrong one of the MID's two grid properties.** The MID publishes both `grid/islanding-state` (`ON_GRID`/`OFF_GRID`/`UNKNOWN`) and `grid/grid-state` (`UP`/`DOWN`/`DEGRADED`/`UNKNOWN`). The flat schema's `grid_state` was the BESS's
|
|
102
|
+
`grid-state`, an islanding answer, so its successor is `islanding-state`; `grid/grid-state` asks whether the utility supply is healthy and is new in v1.0 with no flat equivalent. Matching on the property name rather than the value set put `UP` where a
|
|
103
|
+
consumer expects `ON_GRID` — an entity keeping its id and history while its vocabulary silently changed. `grid/grid-state` is left unmapped, being a new signal rather than a replacement for an existing field.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api-schema-1"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.0b3"
|
|
4
4
|
description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -9,12 +9,17 @@ readme = "README.md"
|
|
|
9
9
|
license = "MIT"
|
|
10
10
|
requires-python = ">=3.10,<4.0"
|
|
11
11
|
dependencies = [
|
|
12
|
-
|
|
12
|
+
# b3, not b2: this parser imports `SpanMidSnapshot`, which 3.0.0b2 does not
|
|
13
|
+
# define. A `>=3.0.0b2` floor lets a resolver pair this wheel with 3.0.0b2 and
|
|
14
|
+
# fail on import -- the precise hazard RELEASE.md warns about under "Releasing
|
|
15
|
+
# every distribution". schema-0 keeps its b2 floor; every name it imports is
|
|
16
|
+
# present there, checked rather than assumed.
|
|
17
|
+
"span-panel-api>=3.0.0b3,<4.0",
|
|
13
18
|
# Only this distribution depends on the eBus SDK. The bootstrap and
|
|
14
19
|
# schema-0 stay clean, so a flat-panel install never pulls it in — which is
|
|
15
20
|
# what bounds the release coupling this dependency introduces to panels on
|
|
16
21
|
# r202633+.
|
|
17
|
-
"ebus-sdk>=0.
|
|
22
|
+
"ebus-sdk>=0.19.0,<1.0",
|
|
18
23
|
]
|
|
19
24
|
|
|
20
25
|
[project.urls]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ebus.energy/schemas/property-catalog.json",
|
|
3
|
+
"schema_version": "property-schema-v1",
|
|
4
|
+
"kind": "capability-catalog",
|
|
5
|
+
"capability": "energy.ebus.capability.breaker",
|
|
6
|
+
"version": "0.1",
|
|
7
|
+
"status": "DRAFT",
|
|
8
|
+
"date": "2026-07-11",
|
|
9
|
+
"properties": {
|
|
10
|
+
"rating": {
|
|
11
|
+
"datatype": "integer",
|
|
12
|
+
"unit": "A",
|
|
13
|
+
"req": "SHOULD",
|
|
14
|
+
"description": "Continuous current rating."
|
|
15
|
+
},
|
|
16
|
+
"poles": {
|
|
17
|
+
"datatype": "integer",
|
|
18
|
+
"req": "MAY",
|
|
19
|
+
"description": "Number of poles (1-4). A US split-phase 240 V breaker is `2`."
|
|
20
|
+
},
|
|
21
|
+
"interrupting-rating": {
|
|
22
|
+
"datatype": "integer",
|
|
23
|
+
"unit": "kA",
|
|
24
|
+
"req": "MAY",
|
|
25
|
+
"description": "Interrupting capacity (kAIC), e.g. `10`, `65`, `100`."
|
|
26
|
+
},
|
|
27
|
+
"protection-functions": {
|
|
28
|
+
"datatype": "enum",
|
|
29
|
+
"format": "OVERCURRENT,SHORT_CIRCUIT,GROUND_FAULT,ARC_FAULT",
|
|
30
|
+
"req": "MAY",
|
|
31
|
+
"description": "Multi-valued set of the protections this breaker provides: `OVERCURRENT`, `SHORT_CIRCUIT`, `GROUND_FAULT` (GFCI), `ARC_FAULT` (AFCI)."
|
|
32
|
+
},
|
|
33
|
+
"trip-curve": {
|
|
34
|
+
"datatype": "enum",
|
|
35
|
+
"format": "B,C,D,K",
|
|
36
|
+
"req": "MAY",
|
|
37
|
+
"description": "Instantaneous trip curve: `B`, `C`, `D`, `K`, …"
|
|
38
|
+
},
|
|
39
|
+
"trip-state": {
|
|
40
|
+
"datatype": "enum",
|
|
41
|
+
"format": "OK,TRIPPED,STUCK,UNKNOWN,CLOSED",
|
|
42
|
+
"req": "SHOULD",
|
|
43
|
+
"description": "`OK`, `TRIPPED`, `STUCK`, `UNKNOWN`. A tripped breaker carries no current even if a co-located `switch/relay` reads `CLOSED`, so `trip-state` is not a relay state."
|
|
44
|
+
},
|
|
45
|
+
"trip-cause": {
|
|
46
|
+
"datatype": "enum",
|
|
47
|
+
"format": "OVERCURRENT,SHORT_CIRCUIT,GROUND_FAULT,ARC_FAULT,OVERVOLTAGE,UNKNOWN",
|
|
48
|
+
"req": "MAY",
|
|
49
|
+
"description": "Cause of the most recent trip: `OVERCURRENT`, `SHORT_CIRCUIT`, `GROUND_FAULT`, `ARC_FAULT`, `OVERVOLTAGE`, `UNKNOWN`."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ebus.energy/schemas/property-catalog.json",
|
|
3
|
+
"schema_version": "property-schema-v1",
|
|
4
|
+
"kind": "capability-catalog",
|
|
5
|
+
"capability": "energy.ebus.capability.connection",
|
|
6
|
+
"version": "0.1",
|
|
7
|
+
"status": "DRAFT",
|
|
8
|
+
"date": "2026-07-05",
|
|
9
|
+
"properties": {
|
|
10
|
+
"feeds-device-id": {
|
|
11
|
+
"datatype": "string",
|
|
12
|
+
"req": "MAY",
|
|
13
|
+
"description": "Homie device ID of the device wired *downstream* of this connection point. Published only when the specific downstream device is known. Omitted when unknown, when mixed-load with no commissioned downstream device, or when nothing is connected."
|
|
14
|
+
},
|
|
15
|
+
"feeds-device-type": {
|
|
16
|
+
"datatype": "string",
|
|
17
|
+
"req": "MAY",
|
|
18
|
+
"description": "`$description.type` of the downstream device (e.g. `energy.ebus.device.bess`, `.pv`, `.evse`, `.water-heater`, `.distribution-enclosure`, or a DER sub-device such as `.battery`). Published when the class is known even if the specific ID is not."
|
|
19
|
+
},
|
|
20
|
+
"feeds-device-status": {
|
|
21
|
+
"datatype": "enum",
|
|
22
|
+
"format": "OK,LOST,DEGRADED",
|
|
23
|
+
"req": "MAY",
|
|
24
|
+
"description": "Publisher's view of communication-link health to the downstream device: `OK`, `LOST`, `DEGRADED`. Published only when `feeds-device-id` is published and the publisher has a communication integration with that device."
|
|
25
|
+
},
|
|
26
|
+
"fed-by-device-id": {
|
|
27
|
+
"datatype": "string",
|
|
28
|
+
"req": "MAY",
|
|
29
|
+
"description": "Homie device ID of the device wired *upstream* of this connection point. Published only when known (e.g. an upstream BESS wired between the utility and the enclosure, or an upstream sister enclosure in a chain). Omitted when the upstream side is the utility, an implicit busbar, or unknown."
|
|
30
|
+
},
|
|
31
|
+
"fed-by-device-type": {
|
|
32
|
+
"datatype": "string",
|
|
33
|
+
"req": "MAY",
|
|
34
|
+
"description": "`$description.type` of the upstream device. Published with `fed-by-device-id`."
|
|
35
|
+
},
|
|
36
|
+
"fed-by-device-status": {
|
|
37
|
+
"datatype": "enum",
|
|
38
|
+
"format": "OK,LOST,DEGRADED",
|
|
39
|
+
"req": "MAY",
|
|
40
|
+
"description": "Publisher's view of communication-link health to the upstream device. Same value domain and applicability as `feeds-device-status`."
|
|
41
|
+
},
|
|
42
|
+
"backed-up": {
|
|
43
|
+
"datatype": "enum",
|
|
44
|
+
"format": "BACKED_UP,NOT_BACKED_UP,UNKNOWN",
|
|
45
|
+
"req": "MAY",
|
|
46
|
+
"description": "Whether this path is on the backup (island) side of a microgrid interconnect device, and so stays energized off-grid: `BACKED_UP`, `NOT_BACKED_UP`, `UNKNOWN`. A wiring fact (which side of the interconnect), distinct from `load-shed/priority` (a shedding *policy*) and `grid/islanding-state` (the present *state*)."
|
|
47
|
+
},
|
|
48
|
+
"feeds-role": {
|
|
49
|
+
"datatype": "enum",
|
|
50
|
+
"format": "LOADS,SUBPANEL,SOLAR,STORAGE,GENERATOR,MIXED,UNUSED",
|
|
51
|
+
"req": "MAY",
|
|
52
|
+
"description": "Summary role of a downstream node that is **not** published as its own eBus device, or that is surveyed-empty: `LOADS`, `SUBPANEL`, `SOLAR`, `STORAGE`, `GENERATOR`, `MIXED`, `UNUSED`. `UNUSED` positively records \"surveyed, nothing connected\" (which absence cannot express). Complements `feeds-device-*`, which is used when the downstream *is* an eBus device."
|
|
53
|
+
},
|
|
54
|
+
"service-rating": {
|
|
55
|
+
"datatype": "integer",
|
|
56
|
+
"unit": "A",
|
|
57
|
+
"req": "MAY",
|
|
58
|
+
"description": "Utility service rating (service size) at a service-entrance connection point. Distinct from `pcs/feed-import-limit` (a PCS enforcement limit) and `breaker/rating` (a main breaker)."
|
|
59
|
+
},
|
|
60
|
+
"overcurrent-protection": {
|
|
61
|
+
"datatype": "integer",
|
|
62
|
+
"unit": "A",
|
|
63
|
+
"req": "MAY",
|
|
64
|
+
"description": "Overcurrent-protection rating at a connection point that is not itself a breaker-protected circuit (for example a feeder conductor landing in unprotected lugs). Where the connection point *is* a breaker-protected circuit, the rating is `breaker/rating` instead."
|
|
65
|
+
},
|
|
66
|
+
"count": {
|
|
67
|
+
"datatype": "integer",
|
|
68
|
+
"req": "MAY",
|
|
69
|
+
"description": "When the connected node aggregates multiple physical units behind a *single* connection point (e.g. 6 battery packs in one BESS, or 4 microinverters on one AC string reported as one solar device), how many."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|