span-panel-api 3.0.0b1__tar.gz → 3.0.0b2__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.0b1 → span_panel_api-3.0.0b2}/.github/workflows/ci.yml +6 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/release.yml +12 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.pre-commit-config.yaml +9 -2
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/CHANGELOG.md +30 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/DEVELOPMENT.md +22 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/PKG-INFO +9 -2
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/README.md +8 -1
- span_panel_api-3.0.0b2/RELEASE.md +193 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/CHANGELOG.md +17 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/pyproject.toml +2 -2
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/adapter.py +16 -5
- span_panel_api-3.0.0b2/packages/schema-1/CHANGELOG.md +60 -0
- span_panel_api-3.0.0b2/packages/schema-1/README.md +8 -0
- span_panel_api-3.0.0b2/packages/schema-1/pyproject.toml +42 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/__init__.py +6 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/adapter.py +260 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/circuits.py +177 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/const.py +115 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/devices.py +143 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +169 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/panel.py +291 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +155 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/transport.py +194 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pyproject.toml +13 -2
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/verify_adapterless_install.py +13 -2
- span_panel_api-3.0.0b2/scripts/verify_reconnect.py +530 -0
- span_panel_api-3.0.0b2/src/span_panel_api/adapters.py +201 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/auth.py +9 -0
- span_panel_api-3.0.0b2/src/span_panel_api/dispatch.py +74 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/exceptions.py +26 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/factory.py +13 -60
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/models.py +8 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/client.py +56 -26
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/protocol.py +35 -12
- span_panel_api-3.0.0b2/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/conftest.py +36 -10
- span_panel_api-3.0.0b2/tests/fixtures/parent_child_tree.json +225 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_adapters_discovery.py +137 -11
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_detection_auth.py +48 -0
- span_panel_api-3.0.0b2/tests/test_factory_dispatch.py +310 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_client_connection.py +17 -12
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_connect_flow.py +3 -3
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_homie.py +8 -6
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_packaging.py +11 -3
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_protocol_conformance.py +1 -1
- span_panel_api-3.0.0b2/tests/test_schema_one_adapter.py +304 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_circuits.py +194 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_devices.py +169 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_panel.py +266 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_snapshot.py +136 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_transport.py +233 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_schema_zero_adapter.py +3 -1
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/uv.lock +44 -2
- span_panel_api-3.0.0b1/src/span_panel_api/adapters.py +0 -126
- span_panel_api-3.0.0b1/tests/test_factory_dispatch.py +0 -160
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.codefactor +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.codefactor.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.deps-installed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/dependabot.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-approve.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.gitignore +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.markdownlint-cli2.jsonc +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.markdownlint.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.prettierrc.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.vscode/extensions.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.vscode/tasks.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/LICENSE +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/SECURITY.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/conftest.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/developer_attribute_readme.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/openapi.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/README.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/accumulator.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/consumer.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/src/span_panel_api_schema_0/field_metadata.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/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.0b2/packages/schema-1/src/span_panel_api_schema_1}/py.typed +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pytest.ini +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pytest_output.log +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/coverage.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/format.sh +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/format_markdown.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/test_live_auth.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/setup-hooks.sh +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/homie_schema.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_public_api_unchanged.py +0 -0
- {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_schema_provenance.py +0 -0
|
@@ -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).
|
|
@@ -77,6 +77,10 @@ repos:
|
|
|
77
77
|
- pytest
|
|
78
78
|
- types-PyYAML
|
|
79
79
|
- paho-mqtt
|
|
80
|
+
# schema-1 parses the parent/child tree with the eBus SDK, which
|
|
81
|
+
# ships py.typed — so the hook needs it installed to resolve those
|
|
82
|
+
# types rather than silently reporting import-not-found.
|
|
83
|
+
- ebus-sdk>=0.18.0
|
|
80
84
|
args: ['--config-file=pyproject.toml']
|
|
81
85
|
exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/.*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*'
|
|
82
86
|
|
|
@@ -92,6 +96,9 @@ repos:
|
|
|
92
96
|
- pytest
|
|
93
97
|
- pyyaml
|
|
94
98
|
- paho-mqtt
|
|
99
|
+
# schema-1 imports the eBus SDK; without it here the hook reports
|
|
100
|
+
# import-error for a dependency that is correctly declared.
|
|
101
|
+
- ebus-sdk>=0.18.0
|
|
95
102
|
exclude: '^src/span_panel_api/generated_client/.*|tests/.*|generate_client\.py|scripts/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*'
|
|
96
103
|
|
|
97
104
|
# Check for common security issues
|
|
@@ -108,7 +115,7 @@ repos:
|
|
|
108
115
|
hooks:
|
|
109
116
|
- id: vulture
|
|
110
117
|
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'
|
|
118
|
+
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
119
|
language: system
|
|
113
120
|
types: [python]
|
|
114
121
|
pass_filenames: false
|
|
@@ -131,6 +138,6 @@ repos:
|
|
|
131
138
|
name: coverage summary
|
|
132
139
|
entry: bash
|
|
133
140
|
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"']
|
|
141
|
+
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
142
|
pass_filenames: false
|
|
136
143
|
verbose: true
|
|
@@ -4,6 +4,36 @@ 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.0b2] - 08/2026
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **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
|
|
14
|
+
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
|
|
15
|
+
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.
|
|
16
|
+
- **`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
|
|
17
|
+
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
|
|
18
|
+
the rejected adapter turns out to be the one required.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`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
|
|
23
|
+
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
|
|
24
|
+
adapter to install, instead of dying inside the flat parser on a missing `energy.ebus.device.circuit/space` property.
|
|
25
|
+
- **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,
|
|
26
|
+
and fills in `data_model_version` / `schema_dispatch_reason` rather than leaving them reading `"not dispatched"`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **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
|
|
31
|
+
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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **`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.
|
|
34
|
+
- **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
|
|
35
|
+
answers "what does this panel need".
|
|
36
|
+
|
|
7
37
|
## [3.0.0b1] - 08/2026
|
|
8
38
|
|
|
9
39
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b2
|
|
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
|
|
@@ -38,10 +38,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
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
|
+
|
|
41
43
|
```bash
|
|
42
|
-
pip install span-panel-api
|
|
44
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
43
45
|
```
|
|
44
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
|
+
|
|
45
52
|
### Dependencies
|
|
46
53
|
|
|
47
54
|
- `httpx` — v2 authentication and detection endpoints
|
|
@@ -23,10 +23,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
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
|
+
|
|
26
28
|
```bash
|
|
27
|
-
pip install span-panel-api
|
|
29
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
28
30
|
```
|
|
29
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
|
+
|
|
30
37
|
### Dependencies
|
|
31
38
|
|
|
32
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 discover_adapters, resolve_adapter, DEFAULT_ADAPTER_KEY
|
|
166
|
+
from span_panel_api.exceptions import SpanPanelAdapterMissingError
|
|
167
|
+
print('adapters:', sorted(discover_adapters()))
|
|
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 discover_adapters
|
|
178
|
+
print('adapters:', sorted(discover_adapters()))
|
|
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,23 @@ 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.0b2] - 08/2026
|
|
11
|
+
|
|
12
|
+
Pre-release. Follows the reshaped `SchemaAdapter` protocol released in `span-panel-api` 3.0.0b2.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`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
|
|
17
|
+
every bootstrap, which is exactly the disagreement the check exists to find.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **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
|
|
22
|
+
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.
|
|
23
|
+
- **`build_field_metadata()` takes no arguments**, reading the schema this adapter was constructed with.
|
|
24
|
+
- **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
|
|
25
|
+
than on argument count inside the transport, but the floor is what stops a resolver reaching it at all.
|
|
26
|
+
|
|
10
27
|
## [1.0.0b1] - 08/2026
|
|
11
28
|
|
|
12
29
|
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.0b2"
|
|
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]
|
|
@@ -16,19 +16,30 @@ from span_panel_api_schema_0.consumer import HomieDeviceConsumer
|
|
|
16
16
|
from span_panel_api_schema_0.field_metadata import build_field_metadata
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
from span_panel_api.models import FieldMetadata,
|
|
19
|
+
from span_panel_api.models import FieldMetadata, SpanPanelSnapshot, V2HomieSchema
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class SchemaZeroAdapter:
|
|
23
23
|
"""Parser for the flat single-device schema (firmware r202603-r202627)."""
|
|
24
24
|
|
|
25
|
+
# A literal, deliberately not imported from span_panel_api.protocol: a value
|
|
26
|
+
# read from the installed bootstrap would agree with every bootstrap, which
|
|
27
|
+
# is the disagreement the check exists to find. Bump when this adapter is
|
|
28
|
+
# rebuilt against a new contract, never to match what happens to be installed.
|
|
29
|
+
ADAPTER_CONTRACT: int = 1
|
|
25
30
|
schema_major = "schema_0"
|
|
26
31
|
SUPPORTS_DATA_MODEL_VERSIONS: tuple[str, str] = (">=0", "<1.0")
|
|
27
32
|
|
|
28
|
-
def __init__(self, serial_number: str,
|
|
33
|
+
def __init__(self, serial_number: str, schema: V2HomieSchema) -> None:
|
|
29
34
|
self._serial_number = serial_number
|
|
35
|
+
# `panel_size` is derived here rather than handed in, because deriving
|
|
36
|
+
# it means reading the flat schema's `types` block for the circuit
|
|
37
|
+
# `space` format — knowledge that belongs to this package. The
|
|
38
|
+
# transport used to do this on every adapter's behalf, which only
|
|
39
|
+
# worked while every adapter was this one.
|
|
40
|
+
self._schema = schema
|
|
30
41
|
self._accumulator = HomiePropertyAccumulator(serial_number)
|
|
31
|
-
self._consumer = HomieDeviceConsumer(self._accumulator, panel_size)
|
|
42
|
+
self._consumer = HomieDeviceConsumer(self._accumulator, schema.panel_size)
|
|
32
43
|
|
|
33
44
|
def topics_to_subscribe(self) -> list[str]:
|
|
34
45
|
return [WILDCARD_TOPIC_FMT.format(serial=self._serial_number)]
|
|
@@ -42,8 +53,8 @@ class SchemaZeroAdapter:
|
|
|
42
53
|
def build_snapshot(self) -> SpanPanelSnapshot:
|
|
43
54
|
return self._consumer.build_snapshot()
|
|
44
55
|
|
|
45
|
-
def build_field_metadata(self
|
|
46
|
-
return build_field_metadata(
|
|
56
|
+
def build_field_metadata(self) -> dict[str, FieldMetadata]:
|
|
57
|
+
return build_field_metadata(self._schema.types)
|
|
47
58
|
|
|
48
59
|
def circuit_nodes_missing_names(self) -> list[str]:
|
|
49
60
|
return self._consumer.circuit_nodes_missing_names()
|
|
@@ -0,0 +1,60 @@
|
|
|
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.0b2] - 08/2026
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`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
|
|
17
|
+
every bootstrap, which is exactly the disagreement the check exists to find.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **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
|
|
22
|
+
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.
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
## [0.1.0b1] - 08/2026
|
|
26
|
+
|
|
27
|
+
Pre-release. First release as a standalone distribution, and the first parser for the parent/child data model.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **`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
|
|
32
|
+
`SpanPanelAdapterMissingError`, so installing it is the opt-in.
|
|
33
|
+
- **`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
|
|
34
|
+
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.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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
|
|
37
|
+
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.
|
|
38
|
+
- **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
|
|
39
|
+
lugs — so the per-device description is what this panel actually has.
|
|
40
|
+
- **A `py.typed` marker**, so consumers type-check against this package's real annotations.
|
|
41
|
+
|
|
42
|
+
### Known deviations and deliberate gaps
|
|
43
|
+
|
|
44
|
+
- **`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
|
|
45
|
+
command instead of publishing where nothing listens; which successor to expose is a product decision.
|
|
46
|
+
- **`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
|
|
47
|
+
validates against a field nothing populates.
|
|
48
|
+
|
|
49
|
+
### Fixed before first release
|
|
50
|
+
|
|
51
|
+
Both found by verifying reconnect against a live broker, and both presented as a healthy connection.
|
|
52
|
+
|
|
53
|
+
- **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
|
|
54
|
+
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
|
|
55
|
+
cannot leak.
|
|
56
|
+
- **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
|
|
57
|
+
does not block a connection; the model is required only when the root's description declares it.
|
|
58
|
+
- **`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
|
|
59
|
+
`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
|
|
60
|
+
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.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# span-panel-api-schema-1
|
|
2
|
+
|
|
3
|
+
Parent/child schema parser (`data-model-version` 1.x, SPAN firmware r202633+) for [span-panel-api](https://github.com/SpanPanel/span-panel-api).
|
|
4
|
+
|
|
5
|
+
**Status: incomplete.** This distribution does not yet register a `schema_1` adapter, so installing it does not make a parent/child panel work. A 1.x panel still raises `SpanPanelAdapterMissingError` naming `schema_1`, which is the honest answer until the
|
|
6
|
+
parser can build a snapshot.
|
|
7
|
+
|
|
8
|
+
What exists today is `BridgeControllerTransport` — an `ebus_sdk.MqttControllerTransport` backed by span-panel-api's own MQTT connection, so the eBus SDK can parse the parent/child tree while the connection to the panel's broker stays ours.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "span-panel-api-schema-1"
|
|
3
|
+
version = "0.1.0b2"
|
|
4
|
+
description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "SpanPanel"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10,<4.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"span-panel-api>=3.0.0b2,<4.0",
|
|
13
|
+
# Only this distribution depends on the eBus SDK. The bootstrap and
|
|
14
|
+
# schema-0 stay clean, so a flat-panel install never pulls it in — which is
|
|
15
|
+
# what bounds the release coupling this dependency introduces to panels on
|
|
16
|
+
# r202633+.
|
|
17
|
+
"ebus-sdk>=0.18.0,<1.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
22
|
+
Issues = "https://github.com/SpanPanel/span-panel-api/issues"
|
|
23
|
+
|
|
24
|
+
# The whole point of this distribution. Dispatch resolves `schema_1` for a 1.x
|
|
25
|
+
# panel by discovering this group, never by importing this package.
|
|
26
|
+
#
|
|
27
|
+
# Held back until the parser could answer for a panel end to end — mapper,
|
|
28
|
+
# field metadata, command topics, and recovery from a real broker outage.
|
|
29
|
+
# Installing this package remains the opt-in: a 1.x panel without it still gets
|
|
30
|
+
# the named SpanPanelAdapterMissingError rather than a silent misparse.
|
|
31
|
+
[project.entry-points."span_panel_api.schema_adapters"]
|
|
32
|
+
schema_1 = "span_panel_api_schema_1:SchemaOneAdapter"
|
|
33
|
+
|
|
34
|
+
[tool.uv.sources]
|
|
35
|
+
span-panel-api = { workspace = true }
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/span_panel_api_schema_1"]
|