span-panel-api 3.0.0b1__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.0b1 → span_panel_api-3.0.0b3}/.github/workflows/ci.yml +6 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/release.yml +12 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.gitignore +6 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.markdownlint-cli2.jsonc +6 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.pre-commit-config.yaml +19 -7
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/CHANGELOG.md +67 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/DEVELOPMENT.md +22 -0
- span_panel_api-3.0.0b1/README.md → span_panel_api-3.0.0b3/PKG-INFO +23 -1
- span_panel_api-3.0.0b1/PKG-INFO → span_panel_api-3.0.0b3/README.md +8 -16
- span_panel_api-3.0.0b3/RELEASE.md +193 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/CHANGELOG.md +33 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/pyproject.toml +2 -2
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/adapter.py +33 -5
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/consumer.py +8 -4
- {span_panel_api-3.0.0b1 → 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.0b3/packages/schema-1/README.md +8 -0
- span_panel_api-3.0.0b3/packages/schema-1/pyproject.toml +47 -0
- 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.0b3/packages/schema-1/src/span_panel_api_schema_1/__init__.py +6 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/adapter.py +311 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/circuits.py +177 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/const.py +127 -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.0b3/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +222 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/panel.py +509 -0
- span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +219 -0
- 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.0b3/packages/schema-1/src/span_panel_api_schema_1/transport.py +194 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pyproject.toml +23 -3
- 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.0b1 → span_panel_api-3.0.0b3}/scripts/verify_adapterless_install.py +17 -7
- span_panel_api-3.0.0b3/scripts/verify_reconnect.py +530 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/__init__.py +2 -0
- span_panel_api-3.0.0b3/src/span_panel_api/adapters.py +232 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/auth.py +9 -0
- span_panel_api-3.0.0b3/src/span_panel_api/dispatch.py +74 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/exceptions.py +26 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/factory.py +18 -61
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/models.py +78 -6
- span_panel_api-3.0.0b3/src/span_panel_api/mqtt/client.py +957 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/protocol.py +47 -12
- span_panel_api-3.0.0b3/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/conftest.py +66 -10
- span_panel_api-3.0.0b3/tests/fixtures/flat_wire.json +563 -0
- span_panel_api-3.0.0b3/tests/fixtures/parent_child_tree.json +225 -0
- span_panel_api-3.0.0b3/tests/test_adapters_discovery.py +398 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_auth_and_homie_helpers.py +0 -1
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_detection_auth.py +48 -1
- span_panel_api-3.0.0b3/tests/test_factory_dispatch.py +310 -0
- span_panel_api-3.0.0b3/tests/test_live_flat_differential.py +166 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_client_connection.py +17 -12
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_connect_flow.py +68 -3
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_homie.py +15 -13
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_packaging.py +11 -3
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_protocol_conformance.py +2 -1
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_protocol_models.py +0 -1
- {span_panel_api-3.0.0b1 → 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.0b3/tests/test_schema_one_adapter.py +465 -0
- 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_circuits.py +194 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_conformance.py +516 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_devices.py +214 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_panel.py +440 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_snapshot.py +152 -0
- span_panel_api-3.0.0b3/tests/test_schema_one_transport.py +233 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_schema_zero_adapter.py +3 -1
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/uv.lock +51 -9
- span_panel_api-3.0.0b1/src/span_panel_api/adapters.py +0 -126
- span_panel_api-3.0.0b1/src/span_panel_api/mqtt/client.py +0 -558
- span_panel_api-3.0.0b1/tests/test_adapters_discovery.py +0 -224
- span_panel_api-3.0.0b1/tests/test_factory_dispatch.py +0 -160
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.codefactor +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.codefactor.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.deps-installed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/dependabot.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-approve.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.markdownlint.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.prettierrc.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.vscode/extensions.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.vscode/tasks.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/LICENSE +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/SECURITY.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/conftest.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/developer_attribute_readme.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/openapi.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/README.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/accumulator.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/src/span_panel_api_schema_0/py.typed +0 -0
- {span_panel_api-3.0.0b1/src/span_panel_api → span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1}/py.typed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pytest.ini +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pytest_output.log +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/coverage.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/format.sh +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/format_markdown.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/test_live_auth.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/setup-hooks.sh +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/homie_schema.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_schema_provenance.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=
|
|
@@ -5,6 +5,12 @@ on:
|
|
|
5
5
|
branches: [ main, develop ]
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [ main, develop ]
|
|
8
|
+
# Push and pull_request both arrive by webhook, so a dropped delivery leaves a
|
|
9
|
+
# commit with no run at all -- which reads the same as a commit that passed.
|
|
10
|
+
# Dispatch re-runs this against any ref on demand.
|
|
11
|
+
#
|
|
12
|
+
# gh workflow run ci.yml --ref develop
|
|
13
|
+
workflow_dispatch:
|
|
8
14
|
|
|
9
15
|
jobs:
|
|
10
16
|
lint-and-test:
|
|
@@ -3,6 +3,18 @@ name: Release
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
+
# A `release: published` event reaches this workflow only through webhook
|
|
7
|
+
# delivery, and a dropped delivery is silent: the release exists, the tag
|
|
8
|
+
# exists, nothing publishes, and the run list looks the same as it did before.
|
|
9
|
+
# Dispatch goes through the API instead, so a release can always be driven to
|
|
10
|
+
# PyPI by hand.
|
|
11
|
+
#
|
|
12
|
+
# gh workflow run release.yml --ref schema-1-v0.1.0b1
|
|
13
|
+
#
|
|
14
|
+
# Dispatch a tag, never a branch. The steps below read the distribution and
|
|
15
|
+
# version out of the tag name, so a branch ref carries neither; it falls
|
|
16
|
+
# through to the error case rather than being guessed at.
|
|
17
|
+
workflow_dispatch:
|
|
6
18
|
|
|
7
19
|
# This repo publishes two distributions that version independently: the
|
|
8
20
|
# bootstrap (span-panel-api) and each schema adapter (span-panel-api-schema-N).
|
|
@@ -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
|
|
@@ -77,6 +82,10 @@ repos:
|
|
|
77
82
|
- pytest
|
|
78
83
|
- types-PyYAML
|
|
79
84
|
- paho-mqtt
|
|
85
|
+
# schema-1 parses the parent/child tree with the eBus SDK, which
|
|
86
|
+
# ships py.typed — so the hook needs it installed to resolve those
|
|
87
|
+
# types rather than silently reporting import-not-found.
|
|
88
|
+
- ebus-sdk>=0.19.0
|
|
80
89
|
args: ['--config-file=pyproject.toml']
|
|
81
90
|
exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/.*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*'
|
|
82
91
|
|
|
@@ -92,6 +101,9 @@ repos:
|
|
|
92
101
|
- pytest
|
|
93
102
|
- pyyaml
|
|
94
103
|
- paho-mqtt
|
|
104
|
+
# schema-1 imports the eBus SDK; without it here the hook reports
|
|
105
|
+
# import-error for a dependency that is correctly declared.
|
|
106
|
+
- ebus-sdk>=0.19.0
|
|
95
107
|
exclude: '^src/span_panel_api/generated_client/.*|tests/.*|generate_client\.py|scripts/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*'
|
|
96
108
|
|
|
97
109
|
# Check for common security issues
|
|
@@ -108,7 +120,7 @@ repos:
|
|
|
108
120
|
hooks:
|
|
109
121
|
- id: vulture
|
|
110
122
|
name: vulture
|
|
111
|
-
entry: bash -c 'uv run vulture src/span_panel_api/ packages/schema-0/src/span_panel_api_schema_0/ --min-confidence 80'
|
|
123
|
+
entry: bash -c 'uv run vulture src/span_panel_api/ packages/schema-0/src/span_panel_api_schema_0/ packages/schema-1/src/span_panel_api_schema_1/ --min-confidence 80'
|
|
112
124
|
language: system
|
|
113
125
|
types: [python]
|
|
114
126
|
pass_filenames: false
|
|
@@ -131,6 +143,6 @@ repos:
|
|
|
131
143
|
name: coverage summary
|
|
132
144
|
entry: bash
|
|
133
145
|
language: system
|
|
134
|
-
args: ['-c', 'output=$(uv run pytest tests/ --cov=src/span_panel_api --cov=packages/schema-0/src/span_panel_api_schema_0 --cov-config=pyproject.toml --cov-fail-under=85 -q 2>&1); status=$?; echo "$output"; exit "$status"']
|
|
146
|
+
args: ['-c', 'output=$(uv run pytest tests/ --cov=src/span_panel_api --cov=packages/schema-0/src/span_panel_api_schema_0 --cov=packages/schema-1/src/span_panel_api_schema_1 --cov-config=pyproject.toml --cov-fail-under=85 -q 2>&1); status=$?; echo "$output"; exit "$status"']
|
|
135
147
|
pass_filenames: false
|
|
136
148
|
verbose: true
|
|
@@ -4,6 +4,73 @@ 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
|
+
|
|
44
|
+
## [3.0.0b2] - 08/2026
|
|
45
|
+
|
|
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.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Adapter contract versioning.** `SchemaAdapter` now requires an `ADAPTER_CONTRACT` integer, and discovery rejects any adapter that does not declare this package's `ADAPTER_CONTRACT_VERSION`. Member presence was never the whole contract: a Protocol
|
|
51
|
+
cannot express signatures at runtime, so an adapter carrying every required name and the previous `__init__` arity passed discovery and failed much later inside the transport, as a bare `TypeError` about an argument count — the least actionable moment to
|
|
52
|
+
learn that two installed packages were built against different versions of each other. Adapters must declare the value as a **literal**; one read from the installed bootstrap would agree with every bootstrap, which is the disagreement being looked for.
|
|
53
|
+
- **`SpanPanelAdapterIncompatibleError`**, raised when the adapter a panel needs is installed but unusable. Distinct from `SpanPanelAdapterMissingError` because the remedy inverts: missing means install something, incompatible means installing more cannot
|
|
54
|
+
help. Reporting the second as the first sends someone to install a package they already have. Discovery still only _logs_ a rejection, so one unusable third-party adapter cannot take down a panel whose own adapter is fine; the error surfaces only when
|
|
55
|
+
the rejected adapter turns out to be the one required.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **`data-model-version` dispatch is live.** The factory hardcoded `None`, so the guard that refuses a parent/child panel was written, tested and never invoked — every panel resolved to the flat parser regardless of what it reported. The Homie schema is
|
|
60
|
+
now fetched over REST **before** the broker is opened and the version drives adapter selection, which SPAN confirmed is a reliable flat-versus-parent/child signal on that endpoint. A `1.0` panel now raises `SpanPanelAdapterMissingError` naming the
|
|
61
|
+
adapter to install, instead of dying inside the flat parser on a missing `energy.ebus.device.circuit/space` property.
|
|
62
|
+
- **A directly constructed `SpanMqttClient` dispatches too.** Building a client without `create_span_client` previously always resolved the flat adapter, so it carried the same defect the factory path had. Dispatch now happens wherever a parser is built,
|
|
63
|
+
and fills in `data_model_version` / `schema_dispatch_reason` rather than leaving them reading `"not dispatched"`.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **BREAKING: `SchemaAdapter.__init__` takes the schema, not a panel size.** `adapter_cls(serial_number, schema)` replaces `adapter_cls(serial_number, panel_size)`. `panel_size` is read out of a block only the flat schema has, so the bootstrap had to
|
|
68
|
+
understand a wire format it is meant to know nothing about, and an adapter whose schema is shaped differently had no way to say so. Each adapter now reads what its own format defines.
|
|
69
|
+
- **BREAKING: `SchemaAdapter.build_field_metadata()` takes no arguments.** It previously received `schema.types` — again a flat-shaped parameter on a format-agnostic protocol. The adapter holds the schema it was constructed with.
|
|
70
|
+
- **`V2HomieSchema.data_model_version`** carries the `dataModelVersion` field, `None` when the panel omits it. Absence is the flat signal and stays distinct from an empty string.
|
|
71
|
+
- **Tier 1 dispatch moved to `span_panel_api.dispatch.select_adapter_key`** from the private `factory._select_adapter_key`, so the transport can dispatch without importing the factory. `adapters.py` continues to answer "what is installed"; the new module
|
|
72
|
+
answers "what does this panel need".
|
|
73
|
+
|
|
7
74
|
## [3.0.0b1] - 08/2026
|
|
8
75
|
|
|
9
76
|
Pre-release. `span-panel-api` becomes a transport and a dispatcher that contains **no parser**. Wire formats ship as separate distributions and register themselves via entry points, so support for a new panel schema arrives by installing a package rather
|
|
@@ -70,6 +70,28 @@ To install pre-commit hooks:
|
|
|
70
70
|
|
|
71
71
|
This installs dependencies (if needed) and configures git pre-commit hooks.
|
|
72
72
|
|
|
73
|
+
## Workspace layout
|
|
74
|
+
|
|
75
|
+
This repository is a uv workspace publishing more than one distribution: the bootstrap (`span-panel-api`, at the root) and one parser package per panel schema (`packages/schema-N/`). `uv sync` installs the workspace, so the test suite runs against every
|
|
76
|
+
distribution together.
|
|
77
|
+
|
|
78
|
+
To work with the packages individually:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Install the workspace including every member
|
|
82
|
+
uv sync --all-packages
|
|
83
|
+
|
|
84
|
+
# Build every distribution
|
|
85
|
+
uv build --all-packages
|
|
86
|
+
|
|
87
|
+
# Build just one
|
|
88
|
+
uv build --package span-panel-api-schema-0
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Releasing
|
|
92
|
+
|
|
93
|
+
See [RELEASE.md](RELEASE.md) — each distribution versions and publishes independently, and the tag name selects which one is published.
|
|
94
|
+
|
|
73
95
|
## Contributing
|
|
74
96
|
|
|
75
97
|
1. Fork and clone the repository
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: span-panel-api
|
|
3
|
+
Version: 3.0.0b3
|
|
4
|
+
Summary: A client library for SPAN Panel API
|
|
5
|
+
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
|
+
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
7
|
+
Author: SpanPanel
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: <4.0,>=3.10
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
1
16
|
# SPAN Panel API
|
|
2
17
|
|
|
3
18
|
[](https://github.com/SpanPanel/span-panel-api/releases)
|
|
@@ -23,10 +38,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
23
38
|
|
|
24
39
|
## Installation
|
|
25
40
|
|
|
41
|
+
Two packages: the transport, and a parser for your panel's schema. `span-panel-api` contains no parser — installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`.
|
|
42
|
+
|
|
26
43
|
```bash
|
|
27
|
-
pip install span-panel-api
|
|
44
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
28
45
|
```
|
|
29
46
|
|
|
47
|
+
`span-panel-api-schema-0` parses the flat schema used by firmware `r202603` through `r202627`, which is every panel in the field today. Panels reporting a `data-model-version` need the adapter for that schema major instead; the error names the one it could
|
|
48
|
+
not find and lists what is installed.
|
|
49
|
+
|
|
50
|
+
Parsers are discovered through the `span_panel_api.schema_adapters` entry-point group, so support for a new panel schema arrives by installing a package rather than by upgrading the transport. The two version independently — see [RELEASE.md](RELEASE.md).
|
|
51
|
+
|
|
30
52
|
### Dependencies
|
|
31
53
|
|
|
32
54
|
- `httpx` — v2 authentication and detection endpoints
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: span-panel-api
|
|
3
|
-
Version: 3.0.0b1
|
|
4
|
-
Summary: A client library for SPAN Panel API
|
|
5
|
-
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
|
-
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
7
|
-
Author: SpanPanel
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Python: <4.0,>=3.10
|
|
11
|
-
Requires-Dist: httpx>=0.28.1
|
|
12
|
-
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
13
|
-
Requires-Dist: pyyaml>=6.0.0
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# SPAN Panel API
|
|
17
2
|
|
|
18
3
|
[](https://github.com/SpanPanel/span-panel-api/releases)
|
|
@@ -38,10 +23,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
38
23
|
|
|
39
24
|
## Installation
|
|
40
25
|
|
|
26
|
+
Two packages: the transport, and a parser for your panel's schema. `span-panel-api` contains no parser — installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`.
|
|
27
|
+
|
|
41
28
|
```bash
|
|
42
|
-
pip install span-panel-api
|
|
29
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
43
30
|
```
|
|
44
31
|
|
|
32
|
+
`span-panel-api-schema-0` parses the flat schema used by firmware `r202603` through `r202627`, which is every panel in the field today. Panels reporting a `data-model-version` need the adapter for that schema major instead; the error names the one it could
|
|
33
|
+
not find and lists what is installed.
|
|
34
|
+
|
|
35
|
+
Parsers are discovered through the `span_panel_api.schema_adapters` entry-point group, so support for a new panel schema arrives by installing a package rather than by upgrading the transport. The two version independently — see [RELEASE.md](RELEASE.md).
|
|
36
|
+
|
|
45
37
|
### Dependencies
|
|
46
38
|
|
|
47
39
|
- `httpx` — v2 authentication and detection endpoints
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This repository publishes **more than one PyPI distribution** from a single source tree. That makes releasing less obvious than `git tag && push`, so this document is the reference: what lives where, how a tag selects what gets published, and what an
|
|
4
|
+
administrator has to do to release everything.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
One repository, one [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/), independent distributions:
|
|
9
|
+
|
|
10
|
+
| Distribution | Directory | Manifest | Purpose |
|
|
11
|
+
| ------------------------- | -------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
|
|
12
|
+
| `span-panel-api` | repository root | `pyproject.toml` | The **bootstrap** — transport, dispatch, protocols. Contains no parser. |
|
|
13
|
+
| `span-panel-api-schema-0` | `packages/schema-0/` | `packages/schema-0/pyproject.toml` | Flat-schema parser (firmware `r202603`–`r202627`) |
|
|
14
|
+
|
|
15
|
+
Adapters are discovered at runtime through the `span_panel_api.schema_adapters` entry-point group. The bootstrap never imports an adapter, and adding an adapter to the field is an install, not an upgrade. Future adapters follow the same pattern under
|
|
16
|
+
`packages/schema-N/`.
|
|
17
|
+
|
|
18
|
+
Consequences for releasing:
|
|
19
|
+
|
|
20
|
+
- **Each distribution has its own version number** in its own manifest.
|
|
21
|
+
- **Each distribution is its own PyPI project**, with its own trusted publisher.
|
|
22
|
+
- **A release publishes exactly one distribution.** Releasing "the repo" means cutting one release per distribution.
|
|
23
|
+
|
|
24
|
+
## Two version axes
|
|
25
|
+
|
|
26
|
+
The bootstrap and the adapters do not share a version, and this is deliberate rather than an oversight.
|
|
27
|
+
|
|
28
|
+
- **The bootstrap** versions on its own library API — the transport and the `SchemaAdapter` protocol.
|
|
29
|
+
- **An adapter** versions on _its_ library API. The wire format it parses is fixed and is declared by `SUPPORTS_DATA_MODEL_VERSIONS`, not by the version number. A release of `span-panel-api-schema-0` means the parser changed, never that the panel did.
|
|
30
|
+
|
|
31
|
+
So `span-panel-api 3.0.0` and `span-panel-api-schema-0 1.0.0` are unrelated numbers, and either can move without the other.
|
|
32
|
+
|
|
33
|
+
Adapters declare a floor on the bootstrap (`span-panel-api>=3.0.0b1,<4.0`). That dependency is why the versions committed in the manifests are load-bearing: they participate in resolution, so they are not placeholders that a release process may overwrite.
|
|
34
|
+
|
|
35
|
+
## How a tag selects a distribution
|
|
36
|
+
|
|
37
|
+
`.github/workflows/release.yml` runs on `release: published` and derives everything from the tag name. There is no lookup table — the manifest path is computed by convention:
|
|
38
|
+
|
|
39
|
+
| Tag | Distribution published | Manifest read |
|
|
40
|
+
| ----------------- | ------------------------- | ---------------------------------- |
|
|
41
|
+
| `vX.Y.Z` | `span-panel-api` | `pyproject.toml` |
|
|
42
|
+
| `schema-N-vX.Y.Z` | `span-panel-api-schema-N` | `packages/schema-N/pyproject.toml` |
|
|
43
|
+
|
|
44
|
+
Worked example for `schema-0-v1.0.0b1`:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
TAG = schema-0-v1.0.0b1
|
|
48
|
+
${TAG#schema-} → 0-v1.0.0b1 strip leading "schema-"
|
|
49
|
+
${SCHEMA%%-v*} → 0 strip trailing "-v…" ⇒ schema number
|
|
50
|
+
PACKAGE = span-panel-api-schema-0
|
|
51
|
+
MANIFEST = packages/schema-0/pyproject.toml
|
|
52
|
+
VERSION = ${TAG#schema-0-v} → 1.0.0b1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Because the schema number is _extracted_ rather than enumerated, a future `schema-1-v0.1.0` resolves to `packages/schema-1/pyproject.toml` with no change to the workflow.
|
|
56
|
+
|
|
57
|
+
A tag matching neither form (`1.2.3`, `nightly`) fails immediately with a message naming both accepted forms.
|
|
58
|
+
|
|
59
|
+
## The tag does not set the version
|
|
60
|
+
|
|
61
|
+
The workflow **verifies** the version; it does not write it.
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
tag schema-0-v1.0.0b1
|
|
65
|
+
⇒ packages/schema-0/pyproject.toml must declare version = "1.0.0b1"
|
|
66
|
+
⇒ otherwise the job fails without publishing
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This means the release ritual is **bump, commit, then tag** — never tag-and-let-CI-stamp. Earlier versions of this workflow rewrote the version from the tag with `sed`, which cannot work here: there is no single manifest to stamp, and the adapter's
|
|
70
|
+
dependency floor on the bootstrap means a stamped version could silently disagree with what resolution actually uses.
|
|
71
|
+
|
|
72
|
+
A mismatch is a hard failure with both numbers in the message, so the common mistake — tagging before committing the bump — is caught before anything reaches PyPI.
|
|
73
|
+
|
|
74
|
+
## Releasing one distribution
|
|
75
|
+
|
|
76
|
+
1. **Bump the version** in that distribution's manifest, and add a `CHANGELOG.md` entry (the root one for the bootstrap, `packages/schema-N/CHANGELOG.md` for an adapter).
|
|
77
|
+
2. **Merge to `develop`** (or `main`, once this work is no longer prototype) and let CI go green.
|
|
78
|
+
3. **Create a GitHub Release:**
|
|
79
|
+
- **Tag** — `vX.Y.Z` or `schema-N-vX.Y.Z`, per the table above.
|
|
80
|
+
- **Target** — the branch holding the bump. This defaults to the repository's default branch, which is the easiest thing to get wrong; a tag cut from the wrong branch builds the wrong version and fails the verification step.
|
|
81
|
+
- **Set as a pre-release** — tick this for any `aN` / `bN` / `rcN` version.
|
|
82
|
+
4. **Watch the run.** `gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')"`
|
|
83
|
+
|
|
84
|
+
The job prints exactly what it resolved, which is the first thing to read if something looks wrong:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Tag 'schema-0-v1.0.0b1' releases span-panel-api-schema-0 1.0.0b1 from packages/schema-0/pyproject.toml
|
|
88
|
+
Version 1.0.0b1 confirmed.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Releasing every distribution
|
|
92
|
+
|
|
93
|
+
There is no "release everything" button, and that is intentional — the distributions version independently, so a coordinated release is a sequence of single-distribution releases rather than one action.
|
|
94
|
+
|
|
95
|
+
To release the whole workspace:
|
|
96
|
+
|
|
97
|
+
1. Bump every manifest that changed, in one branch, with its changelog entry.
|
|
98
|
+
2. If the bootstrap's version moved and adapters need the new floor, update `span-panel-api>=…` in each adapter manifest **in the same branch**. Do not release an adapter whose floor points at a bootstrap version that is not yet on PyPI.
|
|
99
|
+
3. Merge and let CI go green.
|
|
100
|
+
4. Cut the releases **bootstrap first, then each adapter**:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
v3.0.0b1 → span-panel-api
|
|
104
|
+
schema-0-v1.0.0b1 → span-panel-api-schema-0
|
|
105
|
+
schema-1-v0.1.0 → span-panel-api-schema-1
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
PyPI accepts them in any order, but bootstrap-first means there is never a window in which an adapter is installable and its dependency is not.
|
|
109
|
+
|
|
110
|
+
5. Verify from PyPI rather than from CI — see below.
|
|
111
|
+
|
|
112
|
+
Only bump and release what actually changed. A distribution with no changes does not need a release just because a sibling had one.
|
|
113
|
+
|
|
114
|
+
## Adding a new adapter
|
|
115
|
+
|
|
116
|
+
When `packages/schema-N/` lands, the workflow needs no edit — but PyPI does, and this is the step that will be forgotten:
|
|
117
|
+
|
|
118
|
+
1. **Create the PyPI project and its trusted publisher before the first release.** Because the project does not exist yet, this is a _pending publisher_, added from account/organization publishing settings rather than from the (non-existent) project page:
|
|
119
|
+
|
|
120
|
+
| Field | Value |
|
|
121
|
+
| ----------------- | ------------------------- |
|
|
122
|
+
| PyPI Project Name | `span-panel-api-schema-N` |
|
|
123
|
+
| Owner | `SpanPanel` |
|
|
124
|
+
| Repository name | `span-panel-api` |
|
|
125
|
+
| Workflow name | `release.yml` |
|
|
126
|
+
| Environment name | `release` |
|
|
127
|
+
|
|
128
|
+
Every field except the project name is identical across all distributions here, since they all publish from the same repository and workflow. Once the project exists, the same entry is visible and editable at
|
|
129
|
+
`https://pypi.org/manage/project/<name>/settings/publishing/`.
|
|
130
|
+
|
|
131
|
+
2. **Add the package to the workspace** — it is matched by `members = ["packages/*"]` automatically, but the root `[tool.uv.sources]` and the dev dependency group need an entry if the test suite is to exercise it.
|
|
132
|
+
3. **Ship a `py.typed` marker** in the new package. CI fails the build without it.
|
|
133
|
+
|
|
134
|
+
Trusted publishing verifies repository, workflow filename, and environment — it cannot distinguish _which_ distribution a run is building. That is inherent to a monorepo, and it is why the workflow builds only the tagged package: `dist/` never contains a
|
|
135
|
+
sibling that could be uploaded by accident.
|
|
136
|
+
|
|
137
|
+
## What the workflow checks
|
|
138
|
+
|
|
139
|
+
In order, all before anything is uploaded:
|
|
140
|
+
|
|
141
|
+
1. **Tag names a known distribution** — otherwise fail, naming both accepted forms.
|
|
142
|
+
2. **The derived manifest exists** — catches a `schema-N` tag with no matching directory.
|
|
143
|
+
3. **The tag version equals the committed version** — read with `tomllib`, compared exactly.
|
|
144
|
+
4. **Only the tagged package is built** — `uv build --package <name>`, so `dist/` holds exactly one distribution.
|
|
145
|
+
5. **Every built wheel ships `py.typed`** — a fully annotated distribution that omits it resolves as `Any` for every downstream consumer, silently undoing the strict typing this repository maintains.
|
|
146
|
+
|
|
147
|
+
| Failure | Meaning |
|
|
148
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `Tag '…' names no distribution` | Tag is malformed. Use `vX.Y.Z` or `schema-N-vX.Y.Z`. |
|
|
150
|
+
| `resolves to '…', which does not exist` | Tag names a schema whose directory is not in this commit — usually a tag cut from the wrong branch. |
|
|
151
|
+
| `declares version 'A' but the tag says 'B'` | The bump was not committed, or the release targets the wrong branch. |
|
|
152
|
+
| `ships no py.typed marker` | The new package is missing the marker file. |
|
|
153
|
+
| OIDC / trusted publishing rejection | The PyPI publisher for that project is missing or does not match. Nothing was uploaded; fix and re-run the job. |
|
|
154
|
+
|
|
155
|
+
A failed release is safe. Every check runs before upload, so a failure means nothing reached PyPI and the same tag can be re-run once the cause is fixed.
|
|
156
|
+
|
|
157
|
+
## Verifying a release
|
|
158
|
+
|
|
159
|
+
CI going green proves the build, not the install. The seam this repository is built around — a bootstrap that finds a parser it never imports — can only be exercised across a real package boundary, so verify from PyPI:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 1. The bootstrap alone must fail by name, not with ModuleNotFoundError
|
|
163
|
+
python3 -m venv .solo && ./.solo/bin/pip install --pre span-panel-api
|
|
164
|
+
./.solo/bin/python -c "
|
|
165
|
+
from span_panel_api.adapters import installed_adapter_keys, resolve_adapter, DEFAULT_ADAPTER_KEY
|
|
166
|
+
from span_panel_api.exceptions import SpanPanelAdapterMissingError
|
|
167
|
+
print('adapters:', installed_adapter_keys())
|
|
168
|
+
try:
|
|
169
|
+
resolve_adapter(DEFAULT_ADAPTER_KEY, 'release check')
|
|
170
|
+
except SpanPanelAdapterMissingError as exc:
|
|
171
|
+
print('raised as designed:', exc.needed, exc.available)
|
|
172
|
+
"
|
|
173
|
+
|
|
174
|
+
# 2. Both packages: the adapter resolves through discovery
|
|
175
|
+
python3 -m venv .both && ./.both/bin/pip install --pre span-panel-api span-panel-api-schema-0
|
|
176
|
+
./.both/bin/python -c "
|
|
177
|
+
from span_panel_api.adapters import installed_adapter_keys
|
|
178
|
+
print('adapters:', installed_adapter_keys())
|
|
179
|
+
"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Expected: `adapters: []` then a named `SpanPanelAdapterMissingError` in the first, `adapters: ['schema_0']` in the second.
|
|
183
|
+
|
|
184
|
+
Drop `--pre` once the versions being verified are not pre-releases.
|
|
185
|
+
|
|
186
|
+
## Pre-releases
|
|
187
|
+
|
|
188
|
+
Versions like `3.0.0b1` are pre-releases in both places that matter:
|
|
189
|
+
|
|
190
|
+
- **PyPI** will not install them without `--pre`, so `pip install span-panel-api` continues to resolve the last stable release.
|
|
191
|
+
- **GitHub** should have "Set as a pre-release" ticked, which keeps them out of the repository's "Latest release" slot.
|
|
192
|
+
|
|
193
|
+
The publish workflow itself does not care — `on: release: published` fires either way.
|
|
@@ -7,6 +7,39 @@ 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
|
+
|
|
26
|
+
## [1.0.0b2] - 08/2026
|
|
27
|
+
|
|
28
|
+
Pre-release. Follows the reshaped `SchemaAdapter` protocol released in `span-panel-api` 3.0.0b2.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`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
|
|
33
|
+
every bootstrap, which is exactly the disagreement the check exists to find.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **BREAKING: `SchemaZeroAdapter(serial_number, schema)`** replaces `SchemaZeroAdapter(serial_number, panel_size)`, following the protocol change in `span-panel-api`. Panel size is now derived here, by reading the circuit `space` format out of the flat
|
|
38
|
+
schema's `types` block — knowledge that belongs to this package rather than to the transport, which was previously doing it on every adapter's behalf.
|
|
39
|
+
- **`build_field_metadata()` takes no arguments**, reading the schema this adapter was constructed with.
|
|
40
|
+
- **The `span-panel-api` floor is now `>=3.0.0b2`.** `1.0.0b1` declared `>=3.0.0b1`, which admitted a bootstrap that constructs adapters with `panel_size` — a pairing that could not work. Installing that combination now fails by name at discovery rather
|
|
41
|
+
than on argument count inside the transport, but the floor is what stops a resolver reaching it at all.
|
|
42
|
+
|
|
10
43
|
## [1.0.0b1] - 08/2026
|
|
11
44
|
|
|
12
45
|
Pre-release. First release as a standalone distribution.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api-schema-0"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.0b3"
|
|
4
4
|
description = "Flat-schema (data-model-version absent) parser for span-panel-api"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -9,7 +9,7 @@ readme = "README.md"
|
|
|
9
9
|
license = "MIT"
|
|
10
10
|
requires-python = ">=3.10,<4.0"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"span-panel-api>=3.0.
|
|
12
|
+
"span-panel-api>=3.0.0b2,<4.0",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[project.urls]
|