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.
Files changed (153) hide show
  1. span_panel_api-3.0.0b3/.env.example +54 -0
  2. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/ci.yml +6 -0
  3. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/release.yml +12 -0
  4. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.gitignore +6 -0
  5. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.markdownlint-cli2.jsonc +6 -0
  6. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.pre-commit-config.yaml +19 -7
  7. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/CHANGELOG.md +67 -0
  8. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/DEVELOPMENT.md +22 -0
  9. span_panel_api-3.0.0b1/README.md → span_panel_api-3.0.0b3/PKG-INFO +23 -1
  10. span_panel_api-3.0.0b1/PKG-INFO → span_panel_api-3.0.0b3/README.md +8 -16
  11. span_panel_api-3.0.0b3/RELEASE.md +193 -0
  12. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/CHANGELOG.md +33 -0
  13. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/pyproject.toml +2 -2
  14. {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
  15. {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
  16. {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
  17. span_panel_api-3.0.0b3/packages/schema-1/CHANGELOG.md +103 -0
  18. span_panel_api-3.0.0b3/packages/schema-1/README.md +8 -0
  19. span_panel_api-3.0.0b3/packages/schema-1/pyproject.toml +47 -0
  20. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/breaker.json +52 -0
  21. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/connection.json +72 -0
  22. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/door.json +17 -0
  23. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/grid-forming.json +21 -0
  24. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/grid.json +38 -0
  25. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/info.json +52 -0
  26. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/load-shed.json +18 -0
  27. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/meter.json +201 -0
  28. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/pcs.json +111 -0
  29. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/power-flows.json +35 -0
  30. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/shed.json +24 -0
  31. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/soc.json +35 -0
  32. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/status.json +28 -0
  33. span_panel_api-3.0.0b3/packages/schema-1/spec/catalogs/switch.json +29 -0
  34. span_panel_api-3.0.0b3/packages/schema-1/spec/fixtures/simulator_tree.json +4984 -0
  35. span_panel_api-3.0.0b3/packages/schema-1/spec/fixtures/simulator_wire.json +679 -0
  36. span_panel_api-3.0.0b3/packages/schema-1/spec/registries/device-types.md +56 -0
  37. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/__init__.py +6 -0
  38. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/adapter.py +311 -0
  39. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/circuits.py +177 -0
  40. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/const.py +127 -0
  41. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/devices.py +245 -0
  42. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +222 -0
  43. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/panel.py +509 -0
  44. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +219 -0
  45. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json +53 -0
  46. span_panel_api-3.0.0b3/packages/schema-1/src/span_panel_api_schema_1/transport.py +194 -0
  47. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pyproject.toml +23 -3
  48. span_panel_api-3.0.0b3/scripts/capture_flat_reference.py +130 -0
  49. span_panel_api-3.0.0b3/scripts/capture_live_flat.py +147 -0
  50. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/verify_adapterless_install.py +17 -7
  51. span_panel_api-3.0.0b3/scripts/verify_reconnect.py +530 -0
  52. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/__init__.py +2 -0
  53. span_panel_api-3.0.0b3/src/span_panel_api/adapters.py +232 -0
  54. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/auth.py +9 -0
  55. span_panel_api-3.0.0b3/src/span_panel_api/dispatch.py +74 -0
  56. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/exceptions.py +26 -0
  57. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/factory.py +18 -61
  58. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/models.py +78 -6
  59. span_panel_api-3.0.0b3/src/span_panel_api/mqtt/client.py +957 -0
  60. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/protocol.py +47 -12
  61. span_panel_api-3.0.0b3/src/span_panel_api/py.typed +0 -0
  62. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/conftest.py +66 -10
  63. span_panel_api-3.0.0b3/tests/fixtures/flat_wire.json +563 -0
  64. span_panel_api-3.0.0b3/tests/fixtures/parent_child_tree.json +225 -0
  65. span_panel_api-3.0.0b3/tests/test_adapters_discovery.py +398 -0
  66. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_auth_and_homie_helpers.py +0 -1
  67. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_detection_auth.py +48 -1
  68. span_panel_api-3.0.0b3/tests/test_factory_dispatch.py +310 -0
  69. span_panel_api-3.0.0b3/tests/test_live_flat_differential.py +166 -0
  70. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_client_connection.py +17 -12
  71. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_connect_flow.py +68 -3
  72. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_homie.py +15 -13
  73. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_packaging.py +11 -3
  74. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_protocol_conformance.py +2 -1
  75. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_protocol_models.py +0 -1
  76. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_public_api_unchanged.py +3 -0
  77. span_panel_api-3.0.0b3/tests/test_redispatch_on_reconnect.py +301 -0
  78. span_panel_api-3.0.0b3/tests/test_schema_generation_cross_check.py +119 -0
  79. span_panel_api-3.0.0b3/tests/test_schema_migration_delta.py +475 -0
  80. span_panel_api-3.0.0b3/tests/test_schema_one_adapter.py +465 -0
  81. span_panel_api-3.0.0b3/tests/test_schema_one_against_simulator.py +257 -0
  82. span_panel_api-3.0.0b3/tests/test_schema_one_circuits.py +194 -0
  83. span_panel_api-3.0.0b3/tests/test_schema_one_conformance.py +516 -0
  84. span_panel_api-3.0.0b3/tests/test_schema_one_devices.py +214 -0
  85. span_panel_api-3.0.0b3/tests/test_schema_one_panel.py +440 -0
  86. span_panel_api-3.0.0b3/tests/test_schema_one_snapshot.py +152 -0
  87. span_panel_api-3.0.0b3/tests/test_schema_one_transport.py +233 -0
  88. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_schema_zero_adapter.py +3 -1
  89. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/uv.lock +51 -9
  90. span_panel_api-3.0.0b1/src/span_panel_api/adapters.py +0 -126
  91. span_panel_api-3.0.0b1/src/span_panel_api/mqtt/client.py +0 -558
  92. span_panel_api-3.0.0b1/tests/test_adapters_discovery.py +0 -224
  93. span_panel_api-3.0.0b1/tests/test_factory_dispatch.py +0 -160
  94. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.codefactor +0 -0
  95. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.codefactor.yml +0 -0
  96. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.deps-installed +0 -0
  97. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  98. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  99. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/dependabot.yml +0 -0
  100. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-approve.yml +0 -0
  101. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.github/workflows/dependabot-auto-merge.yml +0 -0
  102. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.markdownlint.json +0 -0
  103. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.prettierrc.json +0 -0
  104. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.vscode/extensions.json +0 -0
  105. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/.vscode/tasks.json +0 -0
  106. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/LICENSE +0 -0
  107. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/SECURITY.md +0 -0
  108. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/conftest.py +0 -0
  109. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/developer_attribute_readme.md +0 -0
  110. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/openapi.json +0 -0
  111. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/packages/schema-0/README.md +0 -0
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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
  117. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pytest.ini +0 -0
  118. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/pytest_output.log +0 -0
  119. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/__init__.py +0 -0
  120. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/coverage.py +0 -0
  121. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/format.sh +0 -0
  122. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/format_markdown.py +0 -0
  123. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/scripts/test_live_auth.py +0 -0
  124. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/setup-hooks.sh +0 -0
  125. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/_http.py +0 -0
  126. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/const.py +0 -0
  127. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/detection.py +0 -0
  128. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/__init__.py +0 -0
  129. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/async_client.py +0 -0
  130. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/connection.py +0 -0
  131. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/const.py +0 -0
  132. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/mqtt/models.py +0 -0
  133. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/phase_validation.py +0 -0
  134. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/src/span_panel_api/schema_drift.py +0 -0
  135. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  136. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  137. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  138. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/README.md +0 -0
  139. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/homie_schema.json +0 -0
  140. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/fixtures/v2/status.json +0 -0
  141. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/circuits.response.txt +0 -0
  142. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/panel.response.txt +0 -0
  143. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/soe.response.txt +0 -0
  144. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/simulation_fixtures/status.response.txt +0 -0
  145. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_accumulator.py +0 -0
  146. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_async_mqtt_client.py +0 -0
  147. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_exceptions.py +0 -0
  148. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_field_metadata.py +0 -0
  149. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_bridge.py +0 -0
  150. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_mqtt_debounce.py +0 -0
  151. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_phase_validation_configs.py +0 -0
  152. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b3}/tests/test_phase_validation_errors.py +0 -0
  153. {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
  [![GitHub Release](https://img.shields.io/github/v/release/SpanPanel/span-panel-api?style=flat-square)](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
  [![GitHub Release](https://img.shields.io/github/v/release/SpanPanel/span-panel-api?style=flat-square)](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.0b1"
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.0b1,<4.0",
12
+ "span-panel-api>=3.0.0b2,<4.0",
13
13
  ]
14
14
 
15
15
  [project.urls]