Skip to content

Commit 76123a5

Browse files
committed
release: 3.0.0, with both adapters at 1.0.0
First public release of the split: span-panel-api 3.0.0, span-panel-api-schema-0 1.0.0 and span-panel-api-schema-1 1.0.0. Every dependency floor between them now names a stable version rather than the prerelease it tracked during development -- a specifier naming a prerelease is pip's own signal that prereleases are acceptable for that requirement, so the old floors would have left a released install willing to resolve a future beta of its sibling unasked. Changelogs carry public versions only. Every beta heading is folded into the entry for the version it was working towards, described against the last public release rather than against the beta before it, and a fix that only repaired an earlier beta is gone entirely -- from the point of view of somebody upgrading between released versions it never happened. That collapses thirteen bootstrap betas into one 3.0.0 entry and drops the b8/b9/b10 retry-widening narrative, the _charge_positive rename and the b1-to-b3 discover_adapters churn, none of which a reader upgrading from 2.6.4 has any use for. RELEASE.md states the rule so the next beta does not reintroduce the old shape. Python floor raised to 3.14, matching the only thing that consumes this. Home Assistant requires >=3.14.2 from 2026.3, and the SPAN integration requires HA 2026.8 or newer, so every install reaching this code is on 3.14 whatever we declare. The old >=3.10 was never true in either direction: tests/test_packaging imports tomllib, stdlib only from 3.11, and 3.10 replaces a Protocol's __init__ with (*args, **kwargs), so SchemaAdapter's declared constructor signature is not introspectable there and the check that stops two independently-versioned wheels disagreeing about construction had nothing to read. Floor and CI matrix are now the same version, which is the only arrangement where a green run proves the declared range. mypy's python_version follows the floor for the same reason. Raising the target let ruff apply what it unlocks: asyncio.TimeoutError is the builtin TimeoutError from 3.11, Generator[T, None, None] is Generator[T] from 3.13, and typing.TypeAlias is superseded by the type keyword. The two converted aliases are annotation-only under `from __future__ import annotations`, so nothing resolves them at runtime. The eBus SDK ceiling is now a tested claim. schema-1 declared >=0.19,<0.24 while the lock pinned 0.21.0 and 0.23.1 was current -- and the lock does not ship, so every fresh install resolved the version CI had never run. Upgraded to 0.23.1 (ebus-mqtt-client 0.5.0 with it) and the suite is green, which makes the comment above the bound true again. Documentation corrected against what the code actually does. The README still described HomiePropertyAccumulator, HomieLifecycle and HomieDeviceConsumer as this package's own layers after they moved to schema-0, listed mqtt/accumulator and mqtt/homie in a tree where neither exists, claimed three protocols while showing four, credited a simulation engine removed in 2.3.0, and predated the adapter errors, the widened SpanPanelServerError and the product_name retirement. It gains a section on the hot-loading model, which is the thing a consumer most needs and the thing nothing explained. schema-1's README announced "Status: incomplete -- this distribution does not yet register a schema_1 adapter", which has been false since its first beta and would have shipped on the 1.0.0 page. Also adds span_panel_api_schema_1 to ruff's known-first-party, which the comment directly above it exists to prevent being missed and did not, and adds schema-1 to the coverage measured in CI, which the pre-commit gate covered and the uploaded report did not.
1 parent 8623107 commit 76123a5

21 files changed

Lines changed: 489 additions & 1048 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ jobs:
1717
runs-on: ubuntu-latest
1818
strategy:
1919
matrix:
20+
# One entry because `requires-python` is `>=3.14`: the declared floor and
21+
# the version tested are the same, which is the only arrangement where a
22+
# green run actually proves the range. Widen `requires-python` and this
23+
# list has to grow with it -- a floor no job runs is a claim, not a
24+
# guarantee.
2025
python-version: ["3.14"]
2126

2227
steps:
@@ -64,6 +69,7 @@ jobs:
6469
uv run pytest tests/ -v -rs \
6570
--cov=src/span_panel_api \
6671
--cov=packages/schema-0/src/span_panel_api_schema_0 \
72+
--cov=packages/schema-1/src/span_panel_api_schema_1 \
6773
--cov-report=xml --cov-report=term-missing
6874
6975

CHANGELOG.md

Lines changed: 107 additions & 285 deletions
Large diffs are not rendered by default.

DEVELOPMENT.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22

33
## Prerequisites
44

5-
- Python 3.10+ (CI tests 3.13 and 3.14)
5+
- Python 3.14 (every manifest declares `>=3.14,<4.0`, and CI runs the suite on 3.14)
66
- [uv](https://docs.astral.sh/uv/) for dependency management
77

8+
**The declared floor and the tested version are the same on purpose.** A `requires-python` no job runs is a claim rather than a guarantee, and the two drift apart easily, because every developer and every other workflow here is on the newest interpreter.
9+
Keeping them identical means a green run proves the whole declared range instead of one end of it. If the floor is ever widened, the CI matrix has to widen with it in the same change.
10+
11+
The floor tracks the consumer. Home Assistant requires Python `>=3.12` from 2025.1, `>=3.13.2` from 2025.10 and `>=3.14.2` from 2026.3 — and the SPAN integration that consumes this library requires HA 2026.8 or newer, which puts every install that reaches
12+
this code on 3.14. Declaring anything lower would describe a configuration nobody runs and nothing verifies.
13+
14+
Two older versions are worth naming as specifically ruled out, so that a future "why not support 3.10?" gets answered without re-deriving it. `tests/test_packaging.py` imports `tomllib`, stdlib only from 3.11. More seriously, Python 3.10 replaces a
15+
`Protocol`'s `__init__` with `(*args, **kwargs)`, so `SchemaAdapter`'s declared constructor signature is not introspectable there and `test_schema_adapter_construction_signature_matches_its_implementation` has nothing to read — the check that stops two
16+
independently-versioned wheels disagreeing about how an adapter is constructed would be inert. For a library built around exactly that seam, that is the wrong place to have a hole.
17+
818
## Setup
919

1020
```bash
@@ -271,5 +281,14 @@ See [RELEASE.md](RELEASE.md) — each distribution versions and publishes indepe
271281
1. Fork and clone the repository
272282
2. Install dev dependencies: `uv sync`
273283
3. Make changes and add tests
274-
4. Ensure all checks pass: `uv run pytest && uv run mypy src/ && uv run ruff check src/`
284+
4. Ensure all checks pass across every distribution, not just the bootstrap:
285+
286+
```bash
287+
uv run pytest
288+
uv run mypy src packages
289+
uv run ruff check .
290+
```
291+
292+
`uv run pre-commit run --all-files` is what CI actually runs, and it covers these plus the markdown, security and dead-code hooks.
293+
275294
5. Submit a pull request

README.md

Lines changed: 108 additions & 38 deletions
Large diffs are not rendered by default.

RELEASE.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ The bootstrap and the adapters do not share a version, and this is deliberate ra
3030

3131
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.
3232

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.
33+
Adapters declare a floor on the bootstrap (`span-panel-api>=3.0.0,<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+
Those floors name **stable** versions on purpose. A specifier that names a prerelease is pip's own signal that prereleases are acceptable for that requirement, so a floor left pointing at a beta would leave a released install willing to resolve a future
36+
beta of its sibling without anyone asking for one.
3437

3538
## How a tag selects a distribution
3639

@@ -73,7 +76,12 @@ A mismatch is a hard failure with both numbers in the message, so the common mis
7376

7477
## Releasing one distribution
7578

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).
79+
1. **Bump the version** in that distribution's manifest, and record the change in its `CHANGELOG.md` (the root one for the bootstrap, `packages/schema-N/CHANGELOG.md` for an adapter).
80+
81+
**Changelogs carry public versions only.** A beta gets no heading of its own: fold its changes into the entry for the public version it is working towards, described against the **last public release** rather than against the beta before it. A fix that
82+
only repairs something an earlier beta broke does not appear at all — from the point of view of somebody upgrading between released versions, it never happened. This keeps the file answering the question a reader actually has ("what changes if I
83+
upgrade?") instead of narrating development.
84+
7785
2. **Merge to `develop`** (or `main`, once this work is no longer prototype) and let CI go green.
7886
3. **Create a GitHub Release:**
7987
- **Tag**`vX.Y.Z` or `schema-N-vX.Y.Z`, per the table above.
@@ -100,9 +108,9 @@ To release the whole workspace:
100108
4. Cut the releases **bootstrap first, then each adapter**:
101109

102110
```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
111+
v3.0.0 → span-panel-api
112+
schema-0-v1.0.0 → span-panel-api-schema-0
113+
schema-1-v1.0.0 → span-panel-api-schema-1
106114
```
107115

108116
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.
@@ -160,7 +168,7 @@ CI going green proves the build, not the install. The seam this repository is bu
160168

161169
```bash
162170
# 1. The bootstrap alone must fail by name, not with ModuleNotFoundError
163-
python3 -m venv .solo && ./.solo/bin/pip install --pre span-panel-api
171+
python3 -m venv .solo && ./.solo/bin/pip install span-panel-api
164172
./.solo/bin/python -c "
165173
from span_panel_api.adapters import installed_adapter_keys, resolve_adapter, DEFAULT_ADAPTER_KEY
166174
from span_panel_api.exceptions import SpanPanelAdapterMissingError
@@ -172,16 +180,24 @@ except SpanPanelAdapterMissingError as exc:
172180
"
173181

174182
# 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
183+
python3 -m venv .both && ./.both/bin/pip install "span-panel-api[schema-0]"
176184
./.both/bin/python -c "
177185
from span_panel_api.adapters import installed_adapter_keys
178186
print('adapters:', installed_adapter_keys())
179187
"
188+
189+
# 3. The extra is the upgrade path, so check it resolves the adapter too
190+
python3 -m venv .all && ./.all/bin/pip install "span-panel-api[schema-0,schema-1]"
191+
./.all/bin/python -c "
192+
from span_panel_api.adapters import installed_adapter_keys
193+
print('adapters:', installed_adapter_keys())
194+
"
180195
```
181196

182-
Expected: `adapters: []` then a named `SpanPanelAdapterMissingError` in the first, `adapters: ['schema_0']` in the second.
197+
Expected: `adapters: []` then a named `SpanPanelAdapterMissingError` in the first, `adapters: ['schema_0']` in the second, and both keys in the third.
183198

184-
Drop `--pre` once the versions being verified are not pre-releases.
199+
Add `--pre` only when the versions being verified are pre-releases. It is not the default verb any more: from 3.0.0 onwards every distribution here publishes stable versions, and no floor in any manifest names a prerelease — which is deliberate, since a
200+
specifier that names one is pip's own signal that prereleases are acceptable for that requirement.
185201

186202
## Pre-releases
187203

conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
@pytest.fixture
14-
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
14+
def event_loop() -> Generator[asyncio.AbstractEventLoop]:
1515
"""Provide a new asyncio event loop for each test (for pytest-homeassistant compatibility)."""
1616
loop = asyncio.new_event_loop()
1717
yield loop

packages/schema-0/CHANGELOG.md

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
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`
88
rather than by this version number. A release here means this parser changed, never that the panel did.
99

10-
## [1.0.0b5] - 08/2026
10+
Pre-releases are not listed separately. A beta is a step towards the next public version, so its changes are folded into that version's entry as they land and are described against the last public release, never against the beta before it.
1111

12-
Pre-release. Requires `span-panel-api` 3.0.0b4 or newer, which is the floor the manifest has carried since the bootstrap grew the EVSE charge-limit members.
12+
## [1.0.0]
1313

14-
### Added
15-
16-
- **`set_evse_charge_limit_topic` and `evse_charge_limit_payload`.** Both required of every adapter, because `SchemaAdapter` gained them: `_derive_required_members` makes each public protocol member mandatory of every adapter wheel, so an adapter without
17-
them is rejected at discovery no matter which panel it would have parsed. Flat firmware publishes no charge-limit surface, so this distribution answers for the absence rather than for a topic — the point being that answering is not optional.
18-
- **`adopted_devices` reports empty.** Adoption is a parent/child idea: a flat panel is one device with no unmodelled children to adopt, so the honest answer is a stable empty tuple rather than an unimplemented member. `set_adopted_property` therefore
19-
raises on a flat panel for the same reason it raises for a device that does not exist, which is what makes the snapshot lookup an authorization rather than a lookup.
20-
21-
## [1.0.0b3] - 08/2026
22-
23-
Pre-release. Requires `span-panel-api` 3.0.0b2 or newer — unchanged, because nothing added here reaches for anything newer.
24-
25-
### Changed
26-
27-
- **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
28-
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
29-
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
30-
identically on both adapters, so for that device class identity stops being a migration delta at all.
31-
32-
### Added
33-
34-
- **`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
35-
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.
36-
37-
## [1.0.0b2] - 08/2026
38-
39-
Pre-release. Follows the reshaped `SchemaAdapter` protocol released in `span-panel-api` 3.0.0b2.
14+
First release as a standalone distribution. Requires `span-panel-api` 3.0.0 or newer.
4015

4116
### Added
4217

18+
- **The flat-schema parser, extracted from `span-panel-api` 2.6.4.** Relocated from `span_panel_api._impl.schema_0` to `span_panel_api_schema_0`, and registered as `schema_0` under the `span_panel_api.schema_adapters` entry-point group, which is the only
19+
way `span-panel-api` reaches it — the bootstrap never imports this package. Installing it is what makes flat-schema panels work; `span-panel-api` alone connects and then raises `SpanPanelAdapterMissingError` naming the adapter it could not find.
20+
- **`HomieLifecycle`, `HomiePropertyAccumulator` and `HomieDeviceConsumer` live here now.** All three left the bootstrap because they are flat-schema-specific rather than Homie-convention-level: the accumulator filters every topic against a single device's
21+
prefix and stores `node → prop`, and `HomieLifecycle`'s members are not Homie 5 `$state` values but a consumer-side progression encoding "one description received ⇒ ready".
22+
- **`SCHEMA_ANCHOR`** (`sha256:d347556a07d98f40`, firmware `spanos2/r202603/05`) — the schema revision every hardcoded fact in this package was read from, with `SCHEMA_ANCHOR_FIELD` naming the field it comes from (`typesSchemaHash`). The field is
23+
per-adapter: parent/child firmware renames it to `deviceClassesSchemaHash` along with the block it covers, so `schema_1` declares its own rather than inheriting one that does not exist on its firmware.
4324
- **`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
4425
every bootstrap, which is exactly the disagreement the check exists to find.
45-
46-
### Changed
47-
48-
- **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
49-
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.
50-
- **`build_field_metadata()` takes no arguments**, reading the schema this adapter was constructed with.
51-
- **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
52-
than on argument count inside the transport, but the floor is what stops a resolver reaching it at all.
53-
54-
## [1.0.0b1] - 08/2026
55-
56-
Pre-release. First release as a standalone distribution.
57-
58-
### Added
59-
60-
- **The flat-schema parser, extracted from `span-panel-api` 2.6.4.** Relocated verbatim from `span_panel_api._impl.schema_0` to `span_panel_api_schema_0`; only import statements changed. Registers itself as `schema_0` under the
61-
`span_panel_api.schema_adapters` entry-point group, which is the only way `span-panel-api` reaches it — the bootstrap never imports this package.
62-
- **`SCHEMA_ANCHOR`** (`sha256:d347556a07d98f40`, firmware `spanos2/r202603/05`) — the schema revision every hardcoded fact in this package was read from, with `SCHEMA_ANCHOR_FIELD` naming the field it comes from (`typesSchemaHash`). The field is
63-
per-adapter: parent/child firmware renames it to `deviceClassesSchemaHash` along with the block it covers, so a future `schema_1` declares its own rather than inheriting one that does not exist on its firmware.
26+
- **`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
27+
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.
28+
- **`set_evse_charge_limit_topic` and `evse_charge_limit_payload`.** Both are required of every adapter, because `_derive_required_members` makes each public protocol member mandatory of every adapter wheel — an adapter without them is rejected at
29+
discovery no matter which panel it would have parsed. Flat firmware publishes no charge-limit surface, so this distribution answers for the absence rather than for a topic; the point is that answering is not optional.
30+
- **`adopted_devices` reports empty.** Adoption is a parent/child idea: a flat panel is one device with no unmodelled children to adopt, so the honest answer is a stable empty tuple rather than an unimplemented member. `set_adopted_property` therefore
31+
raises on a flat panel for the same reason it raises for a device that does not exist, which is what makes the snapshot lookup an authorization rather than a lookup.
32+
- **Panel size is derived here**, by reading the circuit `space` format out of the flat schema's `types` block — knowledge that belongs to this package rather than to the transport, which previously did it on every adapter's behalf.
6433
- **Provenance tests** asserting that all 64 hardcoded `(node_type, property_id)` pairs still resolve against the captured schema, that `HOMIE_DOMAIN` / `HOMIE_VERSION` still match it, and that the two lugs subtypes real firmware publishes remain absent
6534
from the schema _and_ present in the metadata alias table. This is the only signal that catches schema drift before release; every other symptom reaches production as a silent absence.
6635
- **A `py.typed` marker**, so consumers type-check against this package's real annotations rather than resolving everything it exports as `Any`.
6736

37+
### Changed
38+
39+
- **BREAKING — DER identity is translated into the parent/child 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
40+
is the irregular side: it puts the SKU in `bess/model` and in `evse/part-number` — the same concept under two names — and gives PV neither. Mirroring that would have permanently encoded flat's irregularity in the snapshot, so this adapter normalises
41+
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 identically on both adapters, so for that device class identity
42+
stops being a migration delta at all.
43+
6844
### Known deviations from the published schema
6945

7046
- **Circuit `active-power` is treated as watts, though the schema declares kilowatts.** Real panels publish watts; this was established against live hardware and the 1000× correction was removed accordingly. A test asserts the schema still says `kW`, so

packages/schema-0/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@ support for a new panel schema by installing a package rather than by upgrading
1313
## Installation
1414

1515
```console
16-
pip install span-panel-api span-panel-api-schema-0
16+
pip install "span-panel-api[schema-0]"
1717
```
1818

1919
Installing this package is what makes flat-schema panels work. `span-panel-api` on its own will connect and then raise `SpanPanelAdapterMissingError` naming the adapter it could not find.
2020

2121
A consumer that wants to support panels on either schema installs both adapters:
2222

2323
```console
24-
pip install span-panel-api span-panel-api-schema-0 span-panel-api-schema-1
24+
pip install "span-panel-api[schema-0,schema-1]"
2525
```
2626

27-
Dispatch happens at runtime, per panel, from the `data-model-version` the panel reports.
27+
Dispatch happens at runtime, per panel, from the `data-model-version` the panel reports. The extras are the recommended spelling because they give `pip install -U` a correct upgrade path — the dependency arrow runs from adapter to bootstrap, so upgrading
28+
the bootstrap alone would otherwise leave a stale adapter wheel that discovery then rejects, with pip reporting success. Naming the distributions directly works too.
2829

2930
## Retirement
3031

0 commit comments

Comments
 (0)