span-panel-api 2.6.4__tar.gz → 3.0.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- span_panel_api-3.0.0b1/.github/workflows/ci.yml +130 -0
- span_panel_api-3.0.0b1/.github/workflows/release.yml +107 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.pre-commit-config.yaml +2 -2
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/CHANGELOG.md +50 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/PKG-INFO +1 -1
- span_panel_api-3.0.0b1/packages/schema-0/CHANGELOG.md +34 -0
- span_panel_api-3.0.0b1/packages/schema-0/README.md +32 -0
- span_panel_api-3.0.0b1/packages/schema-0/pyproject.toml +35 -0
- span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0/__init__.py +11 -0
- {span_panel_api-2.6.4/src/span_panel_api/mqtt → span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0}/accumulator.py +2 -1
- span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0/adapter.py +67 -0
- span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0/const.py +83 -0
- span_panel_api-2.6.4/src/span_panel_api/mqtt/homie.py → span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0/consumer.py +9 -3
- {span_panel_api-2.6.4/src/span_panel_api/mqtt → span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0}/field_metadata.py +4 -57
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/pyproject.toml +33 -2
- span_panel_api-3.0.0b1/scripts/verify_adapterless_install.py +77 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/__init__.py +5 -3
- span_panel_api-3.0.0b1/src/span_panel_api/adapters.py +126 -0
- span_panel_api-3.0.0b1/src/span_panel_api/exceptions.py +79 -0
- span_panel_api-3.0.0b1/src/span_panel_api/factory.py +144 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/mqtt/__init__.py +5 -6
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/mqtt/client.py +120 -51
- span_panel_api-3.0.0b1/src/span_panel_api/mqtt/const.py +30 -0
- span_panel_api-3.0.0b1/src/span_panel_api/protocol.py +132 -0
- span_panel_api-3.0.0b1/src/span_panel_api/py.typed +0 -0
- span_panel_api-3.0.0b1/src/span_panel_api/schema_drift.py +65 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/conftest.py +1 -1
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_accumulator.py +2 -2
- span_panel_api-3.0.0b1/tests/test_adapters_discovery.py +224 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_auth_and_homie_helpers.py +2 -2
- span_panel_api-3.0.0b1/tests/test_factory_dispatch.py +160 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_field_metadata.py +2 -1
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_mqtt_client_connection.py +84 -17
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_mqtt_connect_flow.py +11 -14
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_mqtt_homie.py +22 -26
- span_panel_api-3.0.0b1/tests/test_packaging.py +61 -0
- span_panel_api-3.0.0b1/tests/test_protocol_conformance.py +111 -0
- span_panel_api-3.0.0b1/tests/test_public_api_unchanged.py +87 -0
- span_panel_api-3.0.0b1/tests/test_schema_provenance.py +177 -0
- span_panel_api-3.0.0b1/tests/test_schema_zero_adapter.py +53 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/uv.lock +25 -24
- span_panel_api-2.6.4/.github/workflows/ci.yml +0 -101
- span_panel_api-2.6.4/.github/workflows/release.yml +0 -42
- span_panel_api-2.6.4/src/span_panel_api/exceptions.py +0 -42
- span_panel_api-2.6.4/src/span_panel_api/factory.py +0 -73
- span_panel_api-2.6.4/src/span_panel_api/mqtt/const.py +0 -71
- span_panel_api-2.6.4/src/span_panel_api/protocol.py +0 -79
- span_panel_api-2.6.4/tests/test_protocol_conformance.py +0 -48
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.codefactor +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.codefactor.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.deps-installed +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.github/dependabot.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.github/workflows/dependabot-auto-approve.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.gitignore +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.markdownlint-cli2.jsonc +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.markdownlint.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.prettierrc.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.vscode/extensions.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/.vscode/tasks.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/DEVELOPMENT.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/LICENSE +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/README.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/SECURITY.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/conftest.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/developer_attribute_readme.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/openapi.json +0 -0
- {span_panel_api-2.6.4/src/span_panel_api → span_panel_api-3.0.0b1/packages/schema-0/src/span_panel_api_schema_0}/py.typed +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/pytest.ini +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/pytest_output.log +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/scripts/__init__.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/scripts/coverage.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/scripts/format.sh +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/scripts/format_markdown.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/scripts/test_live_auth.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/setup-hooks.sh +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/const.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/models.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/v2/homie_schema.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_detection_auth.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_exceptions.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b1}/tests/test_protocol_models.py +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v7
|
|
19
|
+
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
uses: actions/setup-python@v7
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v7
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: true
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: uv sync --all-packages
|
|
32
|
+
|
|
33
|
+
- name: Run pre-commit hooks
|
|
34
|
+
run: |
|
|
35
|
+
uv run pre-commit run --all-files
|
|
36
|
+
|
|
37
|
+
- name: Run tests with pytest
|
|
38
|
+
run: |
|
|
39
|
+
uv run pytest tests/ -v \
|
|
40
|
+
--cov=src/span_panel_api \
|
|
41
|
+
--cov=packages/schema-0/src/span_panel_api_schema_0 \
|
|
42
|
+
--cov-report=xml --cov-report=term-missing
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
security-scan:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout code
|
|
50
|
+
uses: actions/checkout@v7
|
|
51
|
+
|
|
52
|
+
- name: Set up Python
|
|
53
|
+
uses: actions/setup-python@v7
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.14"
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v7
|
|
59
|
+
with:
|
|
60
|
+
enable-cache: true
|
|
61
|
+
|
|
62
|
+
- name: Install dependencies
|
|
63
|
+
run: uv sync --all-packages
|
|
64
|
+
|
|
65
|
+
- name: Run Bandit security scan
|
|
66
|
+
run: |
|
|
67
|
+
uv run bandit -r src/ packages/ -f json -o bandit-report.json || true
|
|
68
|
+
|
|
69
|
+
- name: Upload Bandit scan results
|
|
70
|
+
uses: actions/upload-artifact@v7
|
|
71
|
+
with:
|
|
72
|
+
name: bandit-report
|
|
73
|
+
path: bandit-report.json
|
|
74
|
+
|
|
75
|
+
build-check:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- name: Checkout code
|
|
79
|
+
uses: actions/checkout@v7
|
|
80
|
+
|
|
81
|
+
- name: Set up Python
|
|
82
|
+
uses: actions/setup-python@v7
|
|
83
|
+
with:
|
|
84
|
+
python-version: "3.14"
|
|
85
|
+
|
|
86
|
+
- name: Install uv
|
|
87
|
+
uses: astral-sh/setup-uv@v7
|
|
88
|
+
with:
|
|
89
|
+
enable-cache: true
|
|
90
|
+
|
|
91
|
+
- name: Install dependencies
|
|
92
|
+
run: uv sync --all-packages
|
|
93
|
+
|
|
94
|
+
- name: Build packages
|
|
95
|
+
run: uv build --all-packages
|
|
96
|
+
|
|
97
|
+
- name: Check packages
|
|
98
|
+
run: uv run twine check dist/*
|
|
99
|
+
|
|
100
|
+
# Every distribution here is fully annotated, so every distribution has to
|
|
101
|
+
# carry the marker that lets a consumer's type checker see those annotations.
|
|
102
|
+
# Without it the package resolves to Any downstream and the typing is inert.
|
|
103
|
+
- name: Verify every wheel ships a py.typed marker
|
|
104
|
+
run: |
|
|
105
|
+
python -c "
|
|
106
|
+
import glob, sys, zipfile
|
|
107
|
+
wheels = glob.glob('dist/*.whl')
|
|
108
|
+
if not wheels:
|
|
109
|
+
sys.exit('::error::no wheels were built')
|
|
110
|
+
for wheel in wheels:
|
|
111
|
+
if not any(n.endswith('/py.typed') for n in zipfile.ZipFile(wheel).namelist()):
|
|
112
|
+
sys.exit(f'::error::{wheel} ships no py.typed marker; downstream type checking would resolve it as Any')
|
|
113
|
+
print(f'{wheel}: py.typed present')
|
|
114
|
+
"
|
|
115
|
+
|
|
116
|
+
# The configuration entry-point discovery exists to support, and the one
|
|
117
|
+
# nothing else in CI exercises: the bootstrap wheel installed with no
|
|
118
|
+
# adapter present. It must import, and it must fail by name rather than
|
|
119
|
+
# with ModuleNotFoundError.
|
|
120
|
+
- name: Verify the bootstrap installs without an adapter
|
|
121
|
+
run: |
|
|
122
|
+
uv venv /tmp/bootstrap-only
|
|
123
|
+
VIRTUAL_ENV=/tmp/bootstrap-only uv pip install dist/span_panel_api-*.whl
|
|
124
|
+
VIRTUAL_ENV=/tmp/bootstrap-only uv run --no-project python scripts/verify_adapterless_install.py
|
|
125
|
+
|
|
126
|
+
- name: Upload build artifacts
|
|
127
|
+
uses: actions/upload-artifact@v7
|
|
128
|
+
with:
|
|
129
|
+
name: dist
|
|
130
|
+
path: dist/
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
# This repo publishes two distributions that version independently: the
|
|
8
|
+
# bootstrap (span-panel-api) and each schema adapter (span-panel-api-schema-N).
|
|
9
|
+
# One release publishes exactly one of them, chosen by the tag prefix:
|
|
10
|
+
#
|
|
11
|
+
# v3.0.0b1 -> span-panel-api (the historical convention)
|
|
12
|
+
# schema-0-v1.0.0b1 -> span-panel-api-schema-0
|
|
13
|
+
#
|
|
14
|
+
# The version lives in the distribution's own pyproject.toml and this workflow
|
|
15
|
+
# only verifies the tag agrees. It deliberately does not rewrite the version at
|
|
16
|
+
# release time: the adapter declares a floor on the bootstrap
|
|
17
|
+
# (span-panel-api>=X), so the committed versions are load-bearing for resolution
|
|
18
|
+
# and cannot be treated as placeholders that a release stamps over.
|
|
19
|
+
#
|
|
20
|
+
# Publishing uses PyPI trusted publishing, which is configured per project. A
|
|
21
|
+
# distribution released here for the first time needs a pending publisher
|
|
22
|
+
# created on PyPI beforehand (project name, this repo, workflow `release.yml`,
|
|
23
|
+
# environment `release`); without it the publish step fails on an otherwise
|
|
24
|
+
# correct build.
|
|
25
|
+
jobs:
|
|
26
|
+
deploy:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: release
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write # For trusted publishing to PyPI
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- name: Checkout code
|
|
34
|
+
uses: actions/checkout@v7
|
|
35
|
+
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
uses: actions/setup-python@v7
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.14"
|
|
40
|
+
|
|
41
|
+
- name: Install uv
|
|
42
|
+
uses: astral-sh/setup-uv@v7
|
|
43
|
+
with:
|
|
44
|
+
enable-cache: true
|
|
45
|
+
|
|
46
|
+
- name: Resolve the distribution from the tag
|
|
47
|
+
id: target
|
|
48
|
+
run: |
|
|
49
|
+
TAG=${GITHUB_REF#refs/tags/}
|
|
50
|
+
case "$TAG" in
|
|
51
|
+
schema-*-v*)
|
|
52
|
+
SCHEMA=${TAG#schema-}
|
|
53
|
+
SCHEMA=${SCHEMA%%-v*}
|
|
54
|
+
PACKAGE="span-panel-api-schema-$SCHEMA"
|
|
55
|
+
MANIFEST="packages/schema-$SCHEMA/pyproject.toml"
|
|
56
|
+
VERSION=${TAG#schema-$SCHEMA-v}
|
|
57
|
+
;;
|
|
58
|
+
v*)
|
|
59
|
+
PACKAGE="span-panel-api"
|
|
60
|
+
MANIFEST="pyproject.toml"
|
|
61
|
+
VERSION=${TAG#v}
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
echo "::error::Tag '$TAG' names no distribution. Use 'vX.Y.Z' for the bootstrap or 'schema-N-vX.Y.Z' for an adapter."
|
|
65
|
+
exit 1
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
69
|
+
echo "::error::Tag '$TAG' resolves to '$MANIFEST', which does not exist."
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
echo "Tag '$TAG' releases $PACKAGE $VERSION from $MANIFEST"
|
|
73
|
+
echo "package=$PACKAGE" >> "$GITHUB_OUTPUT"
|
|
74
|
+
echo "manifest=$MANIFEST" >> "$GITHUB_OUTPUT"
|
|
75
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
76
|
+
|
|
77
|
+
- name: Verify the tag matches the committed version
|
|
78
|
+
run: |
|
|
79
|
+
DECLARED=$(python -c "import sys, tomllib; print(tomllib.load(open(sys.argv[1], 'rb'))['project']['version'])" "${{ steps.target.outputs.manifest }}")
|
|
80
|
+
if [ "$DECLARED" != "${{ steps.target.outputs.version }}" ]; then
|
|
81
|
+
echo "::error::${{ steps.target.outputs.manifest }} declares version '$DECLARED' but the tag says '${{ steps.target.outputs.version }}'. Commit the version bump before tagging."
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
echo "Version $DECLARED confirmed."
|
|
85
|
+
|
|
86
|
+
# Only the tagged distribution is built, so dist/ holds exactly what this
|
|
87
|
+
# release publishes and the publish step cannot pick up a sibling package.
|
|
88
|
+
- name: Build package
|
|
89
|
+
run: uv build --package "${{ steps.target.outputs.package }}"
|
|
90
|
+
|
|
91
|
+
- name: Verify the wheel ships a py.typed marker
|
|
92
|
+
run: |
|
|
93
|
+
python -c "
|
|
94
|
+
import glob, sys, zipfile
|
|
95
|
+
wheels = glob.glob('dist/*.whl')
|
|
96
|
+
if not wheels:
|
|
97
|
+
sys.exit('::error::no wheel was built')
|
|
98
|
+
for wheel in wheels:
|
|
99
|
+
if not any(n.endswith('/py.typed') for n in zipfile.ZipFile(wheel).namelist()):
|
|
100
|
+
sys.exit(f'::error::{wheel} ships no py.typed marker; downstream type checking would resolve it as Any')
|
|
101
|
+
print(f'{wheel}: py.typed present')
|
|
102
|
+
"
|
|
103
|
+
|
|
104
|
+
- name: Publish to PyPI
|
|
105
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
106
|
+
with:
|
|
107
|
+
print-hash: true
|
|
@@ -108,7 +108,7 @@ repos:
|
|
|
108
108
|
hooks:
|
|
109
109
|
- id: vulture
|
|
110
110
|
name: vulture
|
|
111
|
-
entry: bash -c 'uv run vulture src/span_panel_api/ --min-confidence 80'
|
|
111
|
+
entry: bash -c 'uv run vulture src/span_panel_api/ packages/schema-0/src/span_panel_api_schema_0/ --min-confidence 80'
|
|
112
112
|
language: system
|
|
113
113
|
types: [python]
|
|
114
114
|
pass_filenames: false
|
|
@@ -131,6 +131,6 @@ repos:
|
|
|
131
131
|
name: coverage summary
|
|
132
132
|
entry: bash
|
|
133
133
|
language: system
|
|
134
|
-
args: ['-c', 'output=$(uv run pytest tests/ --cov=src/span_panel_api --cov-config=pyproject.toml --cov-fail-under=85 -q 2>&1); status=$?; echo "$output"; exit "$status"']
|
|
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"']
|
|
135
135
|
pass_filenames: false
|
|
136
136
|
verbose: true
|
|
@@ -4,6 +4,56 @@ 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.0b1] - 08/2026
|
|
8
|
+
|
|
9
|
+
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
|
|
10
|
+
than by upgrading the transport. This is prototype work being proven end to end before any decision to land it on `main`.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- **BREAKING: `span-panel-api` no longer contains a parser.** Installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`. Flat-schema panels (firmware `r202603`–`r202627`) need **`span-panel-api-schema-0`** installed
|
|
15
|
+
alongside it:
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **BREAKING: `HomieLifecycle`, `HomiePropertyAccumulator` and `HomieDeviceConsumer` are no longer exported** from `span_panel_api` or `span_panel_api.mqtt`. All three are flat-schema-specific rather than Homie-convention-level: the accumulator filters
|
|
22
|
+
every topic against a single device's prefix and stores `node → prop`, which drops nearly every message under the parent/child model; `HomieLifecycle`'s members are not Homie 5 `$state` values but a consumer-side progression encoding "one description
|
|
23
|
+
received ⇒ ready", which is the flat readiness model. They now live in `span_panel_api_schema_0`.
|
|
24
|
+
- **Removed dead constants** `DEVICE_TOPIC_FMT`, `STATE_TOPIC_FMT`, `DESCRIPTION_TOPIC_FMT`, `PROPERTY_TOPIC_FMT` (unreferenced before the Phase 0 relocation) and `TYPE_PCS` (a real schema type this library does not consume).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`span_panel_api.adapters.resolve_adapter(key, reason)`** — the single place a missing adapter becomes a named error, used by both Tier 1 dispatch and the transport's default path.
|
|
29
|
+
- **`SpanPanelSchemaVersionError`**, raised when a panel reports a `data-model-version` whose schema major cannot be determined. Distinct from `SpanPanelAdapterMissingError` because the remedy differs: a missing adapter is a known schema with no installed
|
|
30
|
+
parser, while this is a schema no adapter can even be named for.
|
|
31
|
+
- **`SpanPanelAdapterMissingError` and `SpanPanelSchemaVersionError` are now exported** from the top-level package — both are errors a user sees when their panel outruns their install, so catching them should not require reaching into a private module.
|
|
32
|
+
- **`SchemaAdapter.__init__` is declared on the protocol.** Construction was always part of the contract (the transport resolves an adapter class from the registry and calls it), but was previously typed only as a `Callable`, leaving the signature
|
|
33
|
+
unchecked against implementations.
|
|
34
|
+
- **Entry-point validation.** `discover_adapters()` now verifies each loaded object is a class implementing the protocol before registering it, and skips it with a logged reason otherwise. One broken third-party adapter cannot take down a panel whose own
|
|
35
|
+
adapter is fine.
|
|
36
|
+
- **`scripts/verify_adapterless_install.py`** and a CI step that runs it against a venv holding only the bootstrap wheel.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`SpanMqttClient(adapter_factory=...)` is now optional.** When omitted, the parser is resolved through entry-point discovery at `_build_adapter()` rather than imported. Resolution is lazy by design: constructing a client must not require an adapter to
|
|
41
|
+
be installed, only building a parser must.
|
|
42
|
+
- **Dispatch refuses an unreadable `data-model-version` instead of assuming flat.** Absence still means the flat schema — that is a real signal, since the property was introduced by the firmware that introduced parent/child. A value whose major _can_ be
|
|
43
|
+
read but whose form is non-canonical (`1`, `1.0-beta`) dispatches on that major and logs the deviation. A value with no extractable major now raises. Previously all three fell through to the flat parser, which does not fail — it produces plausible but
|
|
44
|
+
wrong power and energy figures.
|
|
45
|
+
- **Dispatch diagnostics travel through the `SpanMqttClient` constructor**, removing the window where a connected client reported a selected adapter alongside `schema_dispatch_reason='not dispatched'`.
|
|
46
|
+
- **Releases are now per-distribution and the tag no longer sets the version.** A tag selects which distribution to publish — `vX.Y.Z` for `span-panel-api`, `schema-N-vX.Y.Z` for an adapter — and the release fails unless the tagged version matches the one
|
|
47
|
+
committed in that distribution's `pyproject.toml`. The previous workflow rewrote the root version from the tag and built only the root package, which under a two-distribution layout would have published the bootstrap with no adapter alongside it. Version
|
|
48
|
+
numbers are now load-bearing between the two (the adapter declares a floor on the bootstrap), so they belong in the repository rather than being stamped at release time.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Adapter distributions ship a `py.typed` marker.** Without it a consumer's type checker refuses to read the adapter's annotations and resolves every symbol it exports as `Any`, silently erasing the strict typing at the wheel boundary. CI now fails any
|
|
53
|
+
wheel built without one.
|
|
54
|
+
- **Protocol conformance checking no longer depends on member kind.** The required-member set is derived from every public member `SchemaAdapter` declares, not only the callable ones — a `property` or `classmethod` object is not callable, so the previous
|
|
55
|
+
derivation would have quietly stopped requiring such a member the day the protocol declared one.
|
|
56
|
+
|
|
7
57
|
## [2.6.4] - 05/2026
|
|
8
58
|
|
|
9
59
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0b1
|
|
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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `span-panel-api-schema-0` 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 fixed — the flat single-device schema, SPAN firmware `r202603` through `r202627` — and is identified by `SUPPORTS_DATA_MODEL_VERSIONS`
|
|
8
|
+
rather than by this version number. A release here means this parser changed, never that the panel did.
|
|
9
|
+
|
|
10
|
+
## [1.0.0b1] - 08/2026
|
|
11
|
+
|
|
12
|
+
Pre-release. First release as a standalone distribution.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **The flat-schema parser, extracted from `span-panel-api` 2.6.4.** Relocated verbatim from `span_panel_api._impl.schema_0` to `span_panel_api_schema_0`; only import statements changed. Registers itself as `schema_0` under the
|
|
17
|
+
`span_panel_api.schema_adapters` entry-point group, which is the only way `span-panel-api` reaches it — the bootstrap never imports this package.
|
|
18
|
+
- **`SCHEMA_ANCHOR`** (`sha256:d347556a07d98f40`, firmware `spanos2/r202603/05`) — the schema revision every hardcoded fact in this package was read from, with `SCHEMA_ANCHOR_FIELD` naming the field it comes from (`typesSchemaHash`). The field is
|
|
19
|
+
per-adapter: parent/child firmware renames it to `deviceClassesSchemaHash` along with the block it covers, so a future `schema_1` declares its own rather than inheriting one that does not exist on its firmware.
|
|
20
|
+
- **Provenance tests** asserting that all 64 hardcoded `(node_type, property_id)` pairs still resolve against the captured schema, that `HOMIE_DOMAIN` / `HOMIE_VERSION` still match it, and that the two lugs subtypes real firmware publishes remain absent
|
|
21
|
+
from the schema _and_ present in the metadata alias table. This is the only signal that catches schema drift before release; every other symptom reaches production as a silent absence.
|
|
22
|
+
- **A `py.typed` marker**, so consumers type-check against this package's real annotations rather than resolving everything it exports as `Any`.
|
|
23
|
+
|
|
24
|
+
### Known deviations from the published schema
|
|
25
|
+
|
|
26
|
+
- **Circuit `active-power` is treated as watts, though the schema declares kilowatts.** Real panels publish watts; this was established against live hardware and the 1000× correction was removed accordingly. A test asserts the schema still says `kW`, so
|
|
27
|
+
the day SPAN corrects it we find out rather than discovering it as a factor-of-1000 error.
|
|
28
|
+
- **`energy.ebus.device.lugs.upstream` / `.downstream` are parsed but undeclared.** Firmware publishes these node types in `$description`; the schema declares only the base `energy.ebus.device.lugs`. Property metadata for them resolves through an alias to
|
|
29
|
+
the base type.
|
|
30
|
+
|
|
31
|
+
### Retirement
|
|
32
|
+
|
|
33
|
+
SPAN retires the flat schema in the same firmware release that introduces the parent/child model (`r202633`; fleet rollout projected, not committed, for the first two weeks of September 2026). This package stops being published once the fleet has moved.
|
|
34
|
+
Published versions remain on PyPI for anyone still running older firmware.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# span-panel-api-schema-0
|
|
2
|
+
|
|
3
|
+
The **flat-schema** parser for [`span-panel-api`](https://github.com/SpanPanel/span-panel-api): the single-device Homie model published by SPAN firmware `r202603` through `r202627`, which carries no `data-model-version`.
|
|
4
|
+
|
|
5
|
+
## Why this is a separate distribution
|
|
6
|
+
|
|
7
|
+
`span-panel-api` is a transport and a dispatcher. It knows how to connect to a panel's MQTT broker, route messages, and choose a parser — but it contains no parsing code and no Homie type strings. Each wire format ships as its own distribution and
|
|
8
|
+
registers itself under the `span_panel_api.schema_adapters` entry-point group.
|
|
9
|
+
|
|
10
|
+
That split exists because the two halves break on different axes. The wire format changes when SPAN ships firmware; the library API changes when we do. Separate distributions let each carry its own version, so a consumer can pin them independently and add
|
|
11
|
+
support for a new panel schema by installing a package rather than by upgrading the transport.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Installing this package is what makes flat-schema panels work. `span-panel-api` on its own will connect and then raise `SpanPanelAdapterMissingError` naming the adapter it could not find.
|
|
20
|
+
|
|
21
|
+
A consumer that wants to support panels on either schema installs both adapters:
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
pip install span-panel-api span-panel-api-schema-0 span-panel-api-schema-1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Dispatch happens at runtime, per panel, from the `data-model-version` the panel reports.
|
|
28
|
+
|
|
29
|
+
## Retirement
|
|
30
|
+
|
|
31
|
+
SPAN retires the flat schema in the same release that introduces the parent/child model (`r202633`, fleet rollout projected for early September 2026). When the fleet has moved, consumers drop this package from their requirements. Published versions stay on
|
|
32
|
+
PyPI for anyone still running older firmware.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "span-panel-api-schema-0"
|
|
3
|
+
version = "1.0.0b1"
|
|
4
|
+
description = "Flat-schema (data-model-version absent) 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.0b1,<4.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
17
|
+
Issues = "https://github.com/SpanPanel/span-panel-api/issues"
|
|
18
|
+
|
|
19
|
+
# The whole point of this distribution. The bootstrap finds this adapter by
|
|
20
|
+
# discovering the group, never by importing this package.
|
|
21
|
+
[project.entry-points."span_panel_api.schema_adapters"]
|
|
22
|
+
schema_0 = "span_panel_api_schema_0:SchemaZeroAdapter"
|
|
23
|
+
|
|
24
|
+
# Resolve the bootstrap from the workspace when developing here. Published
|
|
25
|
+
# wheels are unaffected: this table is uv-only metadata and the dependency
|
|
26
|
+
# above is what a consumer installing from PyPI sees.
|
|
27
|
+
[tool.uv.sources]
|
|
28
|
+
span-panel-api = { workspace = true }
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/span_panel_api_schema_0"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Flat-schema adapter package (data-model-version absent)."""
|
|
2
|
+
|
|
3
|
+
from span_panel_api_schema_0.adapter import SchemaZeroAdapter
|
|
4
|
+
|
|
5
|
+
# Re-exported from the adapter rather than restated. The protocol requires the
|
|
6
|
+
# range as a class attribute, so the class is the source of truth; a second
|
|
7
|
+
# literal here would be free to drift, and nothing would notice until a panel
|
|
8
|
+
# reported a version this adapter claims — falsely — to support.
|
|
9
|
+
SUPPORTS_DATA_MODEL_VERSIONS: tuple[str, str] = SchemaZeroAdapter.SUPPORTS_DATA_MODEL_VERSIONS
|
|
10
|
+
|
|
11
|
+
__all__ = ["SUPPORTS_DATA_MODEL_VERSIONS", "SchemaZeroAdapter"]
|
|
@@ -13,7 +13,8 @@ import json
|
|
|
13
13
|
import logging
|
|
14
14
|
import time
|
|
15
15
|
|
|
16
|
-
from .const import HOMIE_STATE_DISCONNECTED, HOMIE_STATE_LOST, HOMIE_STATE_READY
|
|
16
|
+
from span_panel_api.mqtt.const import HOMIE_STATE_DISCONNECTED, HOMIE_STATE_LOST, HOMIE_STATE_READY
|
|
17
|
+
from span_panel_api_schema_0.const import TOPIC_PREFIX
|
|
17
18
|
|
|
18
19
|
_LOGGER = logging.getLogger(__name__)
|
|
19
20
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Flat-schema (data-model-version absent) adapter.
|
|
2
|
+
|
|
3
|
+
Composes the existing accumulator + consumer and owns the flat wire format:
|
|
4
|
+
a single Homie device whose node ids are circuit UUIDs and capability names.
|
|
5
|
+
Nothing outside this package constructs a flat-schema topic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from span_panel_api_schema_0.accumulator import HomiePropertyAccumulator
|
|
14
|
+
from span_panel_api_schema_0.const import PROPERTY_SET_TOPIC_FMT, TYPE_CORE, WILDCARD_TOPIC_FMT
|
|
15
|
+
from span_panel_api_schema_0.consumer import HomieDeviceConsumer
|
|
16
|
+
from span_panel_api_schema_0.field_metadata import build_field_metadata
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from span_panel_api.models import FieldMetadata, HomieSchemaTypes, SpanPanelSnapshot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SchemaZeroAdapter:
|
|
23
|
+
"""Parser for the flat single-device schema (firmware r202603-r202627)."""
|
|
24
|
+
|
|
25
|
+
schema_major = "schema_0"
|
|
26
|
+
SUPPORTS_DATA_MODEL_VERSIONS: tuple[str, str] = (">=0", "<1.0")
|
|
27
|
+
|
|
28
|
+
def __init__(self, serial_number: str, panel_size: int) -> None:
|
|
29
|
+
self._serial_number = serial_number
|
|
30
|
+
self._accumulator = HomiePropertyAccumulator(serial_number)
|
|
31
|
+
self._consumer = HomieDeviceConsumer(self._accumulator, panel_size)
|
|
32
|
+
|
|
33
|
+
def topics_to_subscribe(self) -> list[str]:
|
|
34
|
+
return [WILDCARD_TOPIC_FMT.format(serial=self._serial_number)]
|
|
35
|
+
|
|
36
|
+
def handle_message(self, topic: str, payload: str) -> None:
|
|
37
|
+
self._consumer.handle_message(topic, payload)
|
|
38
|
+
|
|
39
|
+
def is_ready(self) -> bool:
|
|
40
|
+
return self._consumer.is_ready()
|
|
41
|
+
|
|
42
|
+
def build_snapshot(self) -> SpanPanelSnapshot:
|
|
43
|
+
return self._consumer.build_snapshot()
|
|
44
|
+
|
|
45
|
+
def build_field_metadata(self, schema_types: HomieSchemaTypes) -> dict[str, FieldMetadata]:
|
|
46
|
+
return build_field_metadata(schema_types)
|
|
47
|
+
|
|
48
|
+
def circuit_nodes_missing_names(self) -> list[str]:
|
|
49
|
+
return self._consumer.circuit_nodes_missing_names()
|
|
50
|
+
|
|
51
|
+
def find_node_by_type(self, type_str: str) -> str | None:
|
|
52
|
+
return self._consumer.find_node_by_type(type_str)
|
|
53
|
+
|
|
54
|
+
def set_circuit_relay_topic(self, circuit_id: str) -> str:
|
|
55
|
+
return PROPERTY_SET_TOPIC_FMT.format(serial=self._serial_number, node=circuit_id, prop="relay")
|
|
56
|
+
|
|
57
|
+
def set_circuit_priority_topic(self, circuit_id: str) -> str:
|
|
58
|
+
return PROPERTY_SET_TOPIC_FMT.format(serial=self._serial_number, node=circuit_id, prop="shed-priority")
|
|
59
|
+
|
|
60
|
+
def set_dominant_power_source_topic(self) -> str | None:
|
|
61
|
+
core_node = self._consumer.find_node_by_type(TYPE_CORE)
|
|
62
|
+
if core_node is None:
|
|
63
|
+
return None
|
|
64
|
+
return PROPERTY_SET_TOPIC_FMT.format(serial=self._serial_number, node=core_node, prop="dominant-power-source")
|
|
65
|
+
|
|
66
|
+
def register_property_callback(self, callback: Callable[[str, str, str, str | None], None]) -> Callable[[], None]:
|
|
67
|
+
return self._consumer.register_property_callback(callback)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Constants for the flat-schema (Homie v5) parsing implementation."""
|
|
2
|
+
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
# Provenance anchor — the schema revision every fact in this module was read
|
|
5
|
+
# from. `tests/test_schema_provenance.py` fails when a captured schema reports a
|
|
6
|
+
# different one, which is the only pre-release signal that this adapter has
|
|
7
|
+
# drifted from the wire it claims to parse.
|
|
8
|
+
#
|
|
9
|
+
# The field name is per-adapter, not per-bootstrap: flat firmware publishes
|
|
10
|
+
# `typesSchemaHash` over a `types` block, while parent/child renames it to
|
|
11
|
+
# `deviceClassesSchemaHash` over `deviceClasses` — the hash is renamed with the
|
|
12
|
+
# block it covers, so schema_1 declares its own.
|
|
13
|
+
#
|
|
14
|
+
# Content-derived, not build-derived: SPAN defines it as the SHA-256 of the
|
|
15
|
+
# canonicalized schema object and states the schema "may remain unchanged across
|
|
16
|
+
# multiple firmware releases". So it moves when the schema moves, not on every
|
|
17
|
+
# release — which is what makes it usable as an anchor rather than noise.
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
SCHEMA_ANCHOR_FIELD = "typesSchemaHash"
|
|
20
|
+
SCHEMA_ANCHOR = "sha256:d347556a07d98f40"
|
|
21
|
+
SCHEMA_ANCHOR_FIRMWARE = "spanos2/r202603/05"
|
|
22
|
+
|
|
23
|
+
# Homie v5 topic structure
|
|
24
|
+
HOMIE_VERSION = 5
|
|
25
|
+
HOMIE_DOMAIN = "ebus"
|
|
26
|
+
TOPIC_PREFIX = f"{HOMIE_DOMAIN}/{HOMIE_VERSION}"
|
|
27
|
+
|
|
28
|
+
# Topic patterns (serial_number substituted at runtime).
|
|
29
|
+
# The adapter subscribes with the wildcard and publishes with the set pattern;
|
|
30
|
+
# per-topic read formats are not needed because every message arrives through
|
|
31
|
+
# the one wildcard subscription.
|
|
32
|
+
PROPERTY_SET_TOPIC_FMT = f"{TOPIC_PREFIX}/{{serial}}/{{node}}/{{prop}}/set"
|
|
33
|
+
WILDCARD_TOPIC_FMT = f"{TOPIC_PREFIX}/{{serial}}/#"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Homie type strings.
|
|
37
|
+
#
|
|
38
|
+
# Two namespaces that are easy to conflate and are NOT the same set:
|
|
39
|
+
#
|
|
40
|
+
# * the `types` block of GET /api/v2/homie/schema, which declares the
|
|
41
|
+
# properties, units and datatypes available to a type; and
|
|
42
|
+
# * the `type` string a node actually carries in its $description on the wire.
|
|
43
|
+
#
|
|
44
|
+
# Every constant below is a node type observed on the wire. The ones in the
|
|
45
|
+
# first group are also declared in the schema, so metadata lookup finds them
|
|
46
|
+
# directly. See tests/test_schema_provenance.py, which asserts that.
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
TYPE_CORE = "energy.ebus.device.distribution-enclosure.core"
|
|
49
|
+
TYPE_LUGS = "energy.ebus.device.lugs"
|
|
50
|
+
TYPE_CIRCUIT = "energy.ebus.device.circuit"
|
|
51
|
+
TYPE_BESS = "energy.ebus.device.bess"
|
|
52
|
+
TYPE_PV = "energy.ebus.device.pv"
|
|
53
|
+
TYPE_EVSE = "energy.ebus.device.evse"
|
|
54
|
+
TYPE_POWER_FLOWS = "energy.ebus.device.power-flows"
|
|
55
|
+
|
|
56
|
+
# Wire-only subtypes: real node types published by real firmware (confirmed
|
|
57
|
+
# against a live panel in 1eef0dc), but NOT declared in the schema's `types`
|
|
58
|
+
# block, which carries only the base `energy.ebus.device.lugs`. Firmware uses
|
|
59
|
+
# one convention or the other — typed nodes, or generic nodes plus a
|
|
60
|
+
# `direction` property — and _find_lugs_node handles both.
|
|
61
|
+
#
|
|
62
|
+
# Because the schema does not declare them, every one of these needs an entry
|
|
63
|
+
# in field_metadata._LUGS_FALLBACK mapping it to a declared type, or property
|
|
64
|
+
# metadata silently comes back empty for those nodes. The provenance test
|
|
65
|
+
# asserts that pairing rather than trusting it.
|
|
66
|
+
TYPE_LUGS_UPSTREAM = "energy.ebus.device.lugs.upstream"
|
|
67
|
+
TYPE_LUGS_DOWNSTREAM = "energy.ebus.device.lugs.downstream"
|
|
68
|
+
|
|
69
|
+
# Lugs direction values
|
|
70
|
+
LUGS_UPSTREAM = "UPSTREAM"
|
|
71
|
+
LUGS_DOWNSTREAM = "DOWNSTREAM"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_circuit_id(node_id: str) -> str:
|
|
75
|
+
"""Strip dashes from Homie UUID for entity stability."""
|
|
76
|
+
return node_id.replace("-", "")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def denormalize_circuit_id(circuit_id: str) -> str:
|
|
80
|
+
"""Restore dashes to a 32-char dashless UUID (8-4-4-4-12 format)."""
|
|
81
|
+
if len(circuit_id) == 32 and "-" not in circuit_id:
|
|
82
|
+
return f"{circuit_id[:8]}-{circuit_id[8:12]}-{circuit_id[12:16]}-{circuit_id[16:20]}-{circuit_id[20:]}"
|
|
83
|
+
return circuit_id
|
|
@@ -12,9 +12,15 @@ import logging
|
|
|
12
12
|
import time
|
|
13
13
|
from typing import ClassVar
|
|
14
14
|
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
from span_panel_api.models import (
|
|
16
|
+
SpanBatterySnapshot,
|
|
17
|
+
SpanCircuitSnapshot,
|
|
18
|
+
SpanEvseSnapshot,
|
|
19
|
+
SpanPanelSnapshot,
|
|
20
|
+
SpanPVSnapshot,
|
|
21
|
+
)
|
|
22
|
+
from span_panel_api_schema_0.accumulator import HomiePropertyAccumulator
|
|
23
|
+
from span_panel_api_schema_0.const import (
|
|
18
24
|
LUGS_DOWNSTREAM,
|
|
19
25
|
LUGS_UPSTREAM,
|
|
20
26
|
TYPE_BESS,
|