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.
Files changed (123) hide show
  1. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/ci.yml +6 -0
  2. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/release.yml +12 -0
  3. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.pre-commit-config.yaml +9 -2
  4. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/CHANGELOG.md +30 -0
  5. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/DEVELOPMENT.md +22 -0
  6. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/PKG-INFO +9 -2
  7. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/README.md +8 -1
  8. span_panel_api-3.0.0b2/RELEASE.md +193 -0
  9. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/CHANGELOG.md +17 -0
  10. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/pyproject.toml +2 -2
  11. {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
  12. span_panel_api-3.0.0b2/packages/schema-1/CHANGELOG.md +60 -0
  13. span_panel_api-3.0.0b2/packages/schema-1/README.md +8 -0
  14. span_panel_api-3.0.0b2/packages/schema-1/pyproject.toml +42 -0
  15. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/__init__.py +6 -0
  16. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/adapter.py +260 -0
  17. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/circuits.py +177 -0
  18. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/const.py +115 -0
  19. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/devices.py +143 -0
  20. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +169 -0
  21. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/panel.py +291 -0
  22. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +155 -0
  23. span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/transport.py +194 -0
  24. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pyproject.toml +13 -2
  25. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/verify_adapterless_install.py +13 -2
  26. span_panel_api-3.0.0b2/scripts/verify_reconnect.py +530 -0
  27. span_panel_api-3.0.0b2/src/span_panel_api/adapters.py +201 -0
  28. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/auth.py +9 -0
  29. span_panel_api-3.0.0b2/src/span_panel_api/dispatch.py +74 -0
  30. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/exceptions.py +26 -0
  31. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/factory.py +13 -60
  32. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/models.py +8 -0
  33. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/client.py +56 -26
  34. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/protocol.py +35 -12
  35. span_panel_api-3.0.0b2/src/span_panel_api/py.typed +0 -0
  36. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/conftest.py +36 -10
  37. span_panel_api-3.0.0b2/tests/fixtures/parent_child_tree.json +225 -0
  38. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_adapters_discovery.py +137 -11
  39. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_detection_auth.py +48 -0
  40. span_panel_api-3.0.0b2/tests/test_factory_dispatch.py +310 -0
  41. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_client_connection.py +17 -12
  42. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_connect_flow.py +3 -3
  43. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_homie.py +8 -6
  44. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_packaging.py +11 -3
  45. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_protocol_conformance.py +1 -1
  46. span_panel_api-3.0.0b2/tests/test_schema_one_adapter.py +304 -0
  47. span_panel_api-3.0.0b2/tests/test_schema_one_circuits.py +194 -0
  48. span_panel_api-3.0.0b2/tests/test_schema_one_devices.py +169 -0
  49. span_panel_api-3.0.0b2/tests/test_schema_one_panel.py +266 -0
  50. span_panel_api-3.0.0b2/tests/test_schema_one_snapshot.py +136 -0
  51. span_panel_api-3.0.0b2/tests/test_schema_one_transport.py +233 -0
  52. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_schema_zero_adapter.py +3 -1
  53. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/uv.lock +44 -2
  54. span_panel_api-3.0.0b1/src/span_panel_api/adapters.py +0 -126
  55. span_panel_api-3.0.0b1/tests/test_factory_dispatch.py +0 -160
  56. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.codefactor +0 -0
  57. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.codefactor.yml +0 -0
  58. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.deps-installed +0 -0
  59. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  60. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  61. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/dependabot.yml +0 -0
  62. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-approve.yml +0 -0
  63. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-merge.yml +0 -0
  64. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.gitignore +0 -0
  65. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.markdownlint-cli2.jsonc +0 -0
  66. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.markdownlint.json +0 -0
  67. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.prettierrc.json +0 -0
  68. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.vscode/extensions.json +0 -0
  69. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/.vscode/tasks.json +0 -0
  70. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/LICENSE +0 -0
  71. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/SECURITY.md +0 -0
  72. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/conftest.py +0 -0
  73. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/developer_attribute_readme.md +0 -0
  74. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/openapi.json +0 -0
  75. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/packages/schema-0/README.md +0 -0
  76. {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
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pytest.ini +0 -0
  84. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/pytest_output.log +0 -0
  85. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/__init__.py +0 -0
  86. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/coverage.py +0 -0
  87. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/format.sh +0 -0
  88. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/format_markdown.py +0 -0
  89. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/scripts/test_live_auth.py +0 -0
  90. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/setup-hooks.sh +0 -0
  91. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/__init__.py +0 -0
  92. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/_http.py +0 -0
  93. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/const.py +0 -0
  94. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/detection.py +0 -0
  95. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/__init__.py +0 -0
  96. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/async_client.py +0 -0
  97. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/connection.py +0 -0
  98. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/const.py +0 -0
  99. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/models.py +0 -0
  100. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/phase_validation.py +0 -0
  101. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/src/span_panel_api/schema_drift.py +0 -0
  102. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  103. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  104. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  105. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/README.md +0 -0
  106. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/homie_schema.json +0 -0
  107. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/fixtures/v2/status.json +0 -0
  108. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/circuits.response.txt +0 -0
  109. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/panel.response.txt +0 -0
  110. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/soe.response.txt +0 -0
  111. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/status.response.txt +0 -0
  112. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_accumulator.py +0 -0
  113. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_async_mqtt_client.py +0 -0
  114. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_auth_and_homie_helpers.py +0 -0
  115. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_exceptions.py +0 -0
  116. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_field_metadata.py +0 -0
  117. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_bridge.py +0 -0
  118. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_mqtt_debounce.py +0 -0
  119. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_phase_validation_configs.py +0 -0
  120. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_phase_validation_errors.py +0 -0
  121. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_protocol_models.py +0 -0
  122. {span_panel_api-3.0.0b1 → span_panel_api-3.0.0b2}/tests/test_public_api_unchanged.py +0 -0
  123. {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.0b1
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.0b1"
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.0b1,<4.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, HomieSchemaTypes, SpanPanelSnapshot
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, panel_size: int) -> None:
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, schema_types: HomieSchemaTypes) -> dict[str, FieldMetadata]:
46
- return build_field_metadata(schema_types)
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"]
@@ -0,0 +1,6 @@
1
+ """Parent/child schema (data-model-version 1.x) parser for span-panel-api."""
2
+
3
+ from span_panel_api_schema_1.adapter import SchemaOneAdapter
4
+ from span_panel_api_schema_1.transport import ControllerRoutes
5
+
6
+ __all__ = ["ControllerRoutes", "SchemaOneAdapter"]