span-panel-api-schema-0 1.0.0__py3-none-any.whl
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_schema_0/__init__.py +11 -0
- span_panel_api_schema_0/accumulator.py +274 -0
- span_panel_api_schema_0/adapter.py +115 -0
- span_panel_api_schema_0/const.py +83 -0
- span_panel_api_schema_0/consumer.py +647 -0
- span_panel_api_schema_0/field_metadata.py +205 -0
- span_panel_api_schema_0/py.typed +0 -0
- span_panel_api_schema_0-1.0.0.dist-info/METADATA +45 -0
- span_panel_api_schema_0-1.0.0.dist-info/RECORD +11 -0
- span_panel_api_schema_0-1.0.0.dist-info/WHEEL +4 -0
- span_panel_api_schema_0-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Build transport-agnostic field metadata from the Homie schema.
|
|
2
|
+
|
|
3
|
+
Maps every Homie property that ``HomieDeviceConsumer._build_snapshot()``
|
|
4
|
+
reads to a snapshot field path, then looks up the schema-declared unit
|
|
5
|
+
and datatype for each. The result is a dict the integration can consume
|
|
6
|
+
without any Homie knowledge.
|
|
7
|
+
|
|
8
|
+
Field path convention: ``{snapshot_type}.{field_name}``
|
|
9
|
+
- ``panel`` — SpanPanelSnapshot
|
|
10
|
+
- ``circuit`` — SpanCircuitSnapshot
|
|
11
|
+
- ``battery`` — SpanBatterySnapshot
|
|
12
|
+
- ``pv`` — SpanPVSnapshot
|
|
13
|
+
- ``evse`` — SpanEvseSnapshot
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from span_panel_api.models import FieldMetadata, HomieSchemaTypes
|
|
19
|
+
from span_panel_api_schema_0.const import (
|
|
20
|
+
TYPE_BESS,
|
|
21
|
+
TYPE_CIRCUIT,
|
|
22
|
+
TYPE_CORE,
|
|
23
|
+
TYPE_EVSE,
|
|
24
|
+
TYPE_LUGS,
|
|
25
|
+
TYPE_LUGS_DOWNSTREAM,
|
|
26
|
+
TYPE_LUGS_UPSTREAM,
|
|
27
|
+
TYPE_POWER_FLOWS,
|
|
28
|
+
TYPE_PV,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Static mapping: (node_type, property_id) → snapshot field path
|
|
33
|
+
#
|
|
34
|
+
# This encodes the library's internal knowledge of how _build_snapshot()
|
|
35
|
+
# maps Homie properties to snapshot dataclass fields. The mapping must be
|
|
36
|
+
# kept in sync with consumer.py (which held this class as homie.py before
|
|
37
|
+
# the Phase 0 relocation).
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
_PROPERTY_FIELD_MAP: tuple[tuple[str, str, str], ...] = (
|
|
41
|
+
# --- Core node → panel.* -------------------------------------------------
|
|
42
|
+
(TYPE_CORE, "software-version", "panel.firmware_version"),
|
|
43
|
+
(TYPE_CORE, "door", "panel.door_state"),
|
|
44
|
+
(TYPE_CORE, "relay", "panel.main_relay_state"),
|
|
45
|
+
(TYPE_CORE, "ethernet", "panel.eth0_link"),
|
|
46
|
+
(TYPE_CORE, "wifi", "panel.wlan_link"),
|
|
47
|
+
(TYPE_CORE, "vendor-cloud", "panel.vendor_cloud"),
|
|
48
|
+
(TYPE_CORE, "dominant-power-source", "panel.dominant_power_source"),
|
|
49
|
+
(TYPE_CORE, "grid-islandable", "panel.grid_islandable"),
|
|
50
|
+
(TYPE_CORE, "l1-voltage", "panel.l1_voltage"),
|
|
51
|
+
(TYPE_CORE, "l2-voltage", "panel.l2_voltage"),
|
|
52
|
+
(TYPE_CORE, "breaker-rating", "panel.main_breaker_rating_a"),
|
|
53
|
+
(TYPE_CORE, "wifi-ssid", "panel.wifi_ssid"),
|
|
54
|
+
# --- Upstream lugs → panel.* (main meter) --------------------------------
|
|
55
|
+
(TYPE_LUGS_UPSTREAM, "active-power", "panel.instant_grid_power_w"),
|
|
56
|
+
(TYPE_LUGS_UPSTREAM, "imported-energy", "panel.main_meter_energy_consumed_wh"),
|
|
57
|
+
(TYPE_LUGS_UPSTREAM, "exported-energy", "panel.main_meter_energy_produced_wh"),
|
|
58
|
+
(TYPE_LUGS_UPSTREAM, "l1-current", "panel.upstream_l1_current_a"),
|
|
59
|
+
(TYPE_LUGS_UPSTREAM, "l2-current", "panel.upstream_l2_current_a"),
|
|
60
|
+
# --- Downstream lugs → panel.* (feedthrough) -----------------------------
|
|
61
|
+
(TYPE_LUGS_DOWNSTREAM, "active-power", "panel.feedthrough_power_w"),
|
|
62
|
+
(TYPE_LUGS_DOWNSTREAM, "imported-energy", "panel.feedthrough_energy_consumed_wh"),
|
|
63
|
+
(TYPE_LUGS_DOWNSTREAM, "exported-energy", "panel.feedthrough_energy_produced_wh"),
|
|
64
|
+
(TYPE_LUGS_DOWNSTREAM, "l1-current", "panel.downstream_l1_current_a"),
|
|
65
|
+
(TYPE_LUGS_DOWNSTREAM, "l2-current", "panel.downstream_l2_current_a"),
|
|
66
|
+
# --- Circuit → circuit.* -------------------------------------------------
|
|
67
|
+
(TYPE_CIRCUIT, "active-power", "circuit.instant_power_w"),
|
|
68
|
+
(TYPE_CIRCUIT, "exported-energy", "circuit.consumed_energy_wh"),
|
|
69
|
+
(TYPE_CIRCUIT, "imported-energy", "circuit.produced_energy_wh"),
|
|
70
|
+
(TYPE_CIRCUIT, "name", "circuit.name"),
|
|
71
|
+
(TYPE_CIRCUIT, "relay", "circuit.relay_state"),
|
|
72
|
+
(TYPE_CIRCUIT, "shed-priority", "circuit.priority"),
|
|
73
|
+
(TYPE_CIRCUIT, "current", "circuit.current_a"),
|
|
74
|
+
(TYPE_CIRCUIT, "breaker-rating", "circuit.breaker_rating_a"),
|
|
75
|
+
(TYPE_CIRCUIT, "space", "circuit.tabs"),
|
|
76
|
+
(TYPE_CIRCUIT, "sheddable", "circuit.is_sheddable"),
|
|
77
|
+
(TYPE_CIRCUIT, "never-backup", "circuit.is_never_backup"),
|
|
78
|
+
(TYPE_CIRCUIT, "always-on", "circuit.always_on"),
|
|
79
|
+
(TYPE_CIRCUIT, "dipole", "circuit.is_240v"),
|
|
80
|
+
(TYPE_CIRCUIT, "relay-requester", "circuit.relay_requester"),
|
|
81
|
+
# --- BESS → battery.* ----------------------------------------------------
|
|
82
|
+
(TYPE_BESS, "soc", "battery.soe_percentage"),
|
|
83
|
+
(TYPE_BESS, "soe", "battery.soe_kwh"),
|
|
84
|
+
(TYPE_BESS, "vendor-name", "battery.vendor_name"),
|
|
85
|
+
# Flat's irregularity, translated rather than mirrored: it puts the designation in
|
|
86
|
+
# `product-name` and the SKU in `model` on the BESS, where the EVSE puts the SKU in
|
|
87
|
+
# `part-number`. The snapshot speaks v1.0's vocabulary, so both land on the field
|
|
88
|
+
# that matches the concept.
|
|
89
|
+
(TYPE_BESS, "product-name", "battery.model"),
|
|
90
|
+
(TYPE_BESS, "model", "battery.part_number"),
|
|
91
|
+
(TYPE_BESS, "serial-number", "battery.serial_number"),
|
|
92
|
+
(TYPE_BESS, "software-version", "battery.software_version"),
|
|
93
|
+
(TYPE_BESS, "nameplate-capacity", "battery.nameplate_capacity_kwh"),
|
|
94
|
+
(TYPE_BESS, "connected", "battery.connected"),
|
|
95
|
+
(TYPE_BESS, "grid-state", "panel.grid_state"),
|
|
96
|
+
# --- PV → pv.* -----------------------------------------------------------
|
|
97
|
+
(TYPE_PV, "vendor-name", "pv.vendor_name"),
|
|
98
|
+
(TYPE_PV, "product-name", "pv.model"),
|
|
99
|
+
# Declared by the flat schema's `energy.ebus.device.pv` type and read here even
|
|
100
|
+
# though no capture we hold values it — neither the frozen simulator nor the live
|
|
101
|
+
# panel. The row is about what flat *can* say, not what one panel happened to send:
|
|
102
|
+
# `test_der_additions_are_provisional_or_attested_but_never_unexamined` classifies
|
|
103
|
+
# a v1.0-only field by whether flat has a property behind it, and without this row
|
|
104
|
+
# `pv.software_version` would be filed as introduced by v1.0 when flat declares it.
|
|
105
|
+
(TYPE_PV, "software-version", "pv.software_version"),
|
|
106
|
+
(TYPE_PV, "nameplate-capacity", "pv.nameplate_capacity_w"),
|
|
107
|
+
(TYPE_PV, "feed", "pv.feed_circuit_id"),
|
|
108
|
+
(TYPE_PV, "relative-position", "pv.relative_position"), # IN_PANEL | UPSTREAM | DOWNSTREAM
|
|
109
|
+
# --- EVSE → evse.* -------------------------------------------------------
|
|
110
|
+
(TYPE_EVSE, "status", "evse.status"),
|
|
111
|
+
(TYPE_EVSE, "lock-state", "evse.lock_state"),
|
|
112
|
+
(TYPE_EVSE, "advertised-current", "evse.advertised_current_a"),
|
|
113
|
+
(TYPE_EVSE, "vendor-name", "evse.vendor_name"),
|
|
114
|
+
(TYPE_EVSE, "product-name", "evse.model"),
|
|
115
|
+
(TYPE_EVSE, "part-number", "evse.part_number"),
|
|
116
|
+
(TYPE_EVSE, "serial-number", "evse.serial_number"),
|
|
117
|
+
(TYPE_EVSE, "software-version", "evse.software_version"),
|
|
118
|
+
(TYPE_EVSE, "feed", "evse.feed_circuit_id"),
|
|
119
|
+
# --- Power flows → panel.* -----------------------------------------------
|
|
120
|
+
(TYPE_POWER_FLOWS, "pv", "panel.power_flow_pv"),
|
|
121
|
+
(TYPE_POWER_FLOWS, "battery", "panel.power_flow_battery"),
|
|
122
|
+
(TYPE_POWER_FLOWS, "grid", "panel.power_flow_grid"),
|
|
123
|
+
(TYPE_POWER_FLOWS, "site", "panel.power_flow_site"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Generic lugs type used by some firmware versions instead of typed variants.
|
|
127
|
+
# Properties are identical; only the node type differs.
|
|
128
|
+
_LUGS_FALLBACK: dict[str, str] = {
|
|
129
|
+
TYPE_LUGS_UPSTREAM: TYPE_LUGS,
|
|
130
|
+
TYPE_LUGS_DOWNSTREAM: TYPE_LUGS,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _lookup_property(
|
|
135
|
+
schema_types: HomieSchemaTypes,
|
|
136
|
+
node_type: str,
|
|
137
|
+
property_id: str,
|
|
138
|
+
) -> dict[str, object] | None:
|
|
139
|
+
"""Look up a property definition in the schema, with lugs fallback."""
|
|
140
|
+
node_props = schema_types.get(node_type)
|
|
141
|
+
if isinstance(node_props, dict):
|
|
142
|
+
prop_def = node_props.get(property_id)
|
|
143
|
+
if isinstance(prop_def, dict):
|
|
144
|
+
return prop_def
|
|
145
|
+
|
|
146
|
+
# Try generic lugs fallback
|
|
147
|
+
fallback_type = _LUGS_FALLBACK.get(node_type)
|
|
148
|
+
if fallback_type is not None:
|
|
149
|
+
fb_props = schema_types.get(fallback_type)
|
|
150
|
+
if isinstance(fb_props, dict):
|
|
151
|
+
prop_def = fb_props.get(property_id)
|
|
152
|
+
if isinstance(prop_def, dict):
|
|
153
|
+
return prop_def
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _type_declared(schema_types: HomieSchemaTypes, node_type: str) -> bool:
|
|
159
|
+
"""Whether the schema carries a type block a property could have come from.
|
|
160
|
+
|
|
161
|
+
Presence follows the same path `_lookup_property` does, fallback included:
|
|
162
|
+
firmware that publishes only the generic `…device.lugs` block still answers
|
|
163
|
+
for the typed rows, so a property dropped from it is a drop and not absent
|
|
164
|
+
hardware. There is no node dimension here — schema_0's rows address the
|
|
165
|
+
type-level REST schema directly — so the type block is the whole question.
|
|
166
|
+
"""
|
|
167
|
+
if isinstance(schema_types.get(node_type), dict):
|
|
168
|
+
return True
|
|
169
|
+
fallback_type = _LUGS_FALLBACK.get(node_type)
|
|
170
|
+
return fallback_type is not None and isinstance(schema_types.get(fallback_type), dict)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_field_metadata(
|
|
174
|
+
schema_types: HomieSchemaTypes,
|
|
175
|
+
) -> dict[str, FieldMetadata]:
|
|
176
|
+
"""Build field metadata from the Homie schema.
|
|
177
|
+
|
|
178
|
+
Iterates the static property-to-field mapping, looks up each property
|
|
179
|
+
in the schema to get its declared unit and datatype, and returns a dict
|
|
180
|
+
keyed by snapshot field path.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
schema_types: The ``V2HomieSchema.types`` dict.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dict mapping field paths to ``FieldMetadata``.
|
|
187
|
+
"""
|
|
188
|
+
result: dict[str, FieldMetadata] = {}
|
|
189
|
+
|
|
190
|
+
for node_type, property_id, field_path in _PROPERTY_FIELD_MAP:
|
|
191
|
+
prop_def = _lookup_property(schema_types, node_type, property_id)
|
|
192
|
+
if prop_def is None:
|
|
193
|
+
if _type_declared(schema_types, node_type):
|
|
194
|
+
# The type block exists and omits the property — a genuine drop.
|
|
195
|
+
result[field_path] = FieldMetadata(unit=None, datatype="unknown", resolved=False)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
raw_unit = prop_def.get("unit")
|
|
199
|
+
unit = str(raw_unit) if raw_unit is not None else None
|
|
200
|
+
raw_datatype = prop_def.get("datatype")
|
|
201
|
+
datatype = str(raw_datatype) if raw_datatype is not None else "string"
|
|
202
|
+
|
|
203
|
+
result[field_path] = FieldMetadata(unit=unit, datatype=datatype)
|
|
204
|
+
|
|
205
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: span-panel-api-schema-0
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Flat-schema (data-model-version absent) parser 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
|
+
Requires-Python: <4.0,>=3.14
|
|
10
|
+
Requires-Dist: span-panel-api<4.0,>=3.0.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# span-panel-api-schema-0
|
|
14
|
+
|
|
15
|
+
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`.
|
|
16
|
+
|
|
17
|
+
## Why this is a separate distribution
|
|
18
|
+
|
|
19
|
+
`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
|
|
20
|
+
registers itself under the `span_panel_api.schema_adapters` entry-point group.
|
|
21
|
+
|
|
22
|
+
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
|
|
23
|
+
support for a new panel schema by installing a package rather than by upgrading the transport.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
pip install "span-panel-api[schema-0]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
A consumer that wants to support panels on either schema installs both adapters:
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
pip install "span-panel-api[schema-0,schema-1]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Dispatch happens at runtime, per panel, from the `data-model-version` the panel reports. The extras are the recommended spelling because they give `pip install -U` a correct upgrade path — the dependency arrow runs from adapter to bootstrap, so upgrading
|
|
40
|
+
the bootstrap alone would otherwise leave a stale adapter wheel that discovery then rejects, with pip reporting success. Naming the distributions directly works too.
|
|
41
|
+
|
|
42
|
+
## Retirement
|
|
43
|
+
|
|
44
|
+
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
|
|
45
|
+
PyPI for anyone still running older firmware.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
span_panel_api_schema_0/__init__.py,sha256=FRN8R2YS8lUXXm2td4thxqb8OSTh-0_DNCw6XrRyZG0,589
|
|
2
|
+
span_panel_api_schema_0/accumulator.py,sha256=KGkX68hfTMrN0Pv7m_wYEwb7CsxO3vAbG8drJkMk4ao,10424
|
|
3
|
+
span_panel_api_schema_0/adapter.py,sha256=1zOcbOaGAHrP_z3zO-D4vga13lg608khwzFTzHQa6l4,5512
|
|
4
|
+
span_panel_api_schema_0/const.py,sha256=2zswfoyGYNnZy8vQcZ4XB_YRMc9bJlIQImVoiNYcdms,4030
|
|
5
|
+
span_panel_api_schema_0/consumer.py,sha256=Jus285RqeJU3e-pCph0J3u10cixszSZyLybkFSiMdec,27791
|
|
6
|
+
span_panel_api_schema_0/field_metadata.py,sha256=ChsAcGnF56eogRPS-X6HJKqS0nOQWo-vnj3ojSd8aK8,9769
|
|
7
|
+
span_panel_api_schema_0/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
span_panel_api_schema_0-1.0.0.dist-info/METADATA,sha256=PEaY8OsGR1oW9kslIiXc7uIknm9Mc7bS27WOUQFe2fU,2555
|
|
9
|
+
span_panel_api_schema_0-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
10
|
+
span_panel_api_schema_0-1.0.0.dist-info/entry_points.txt,sha256=cj7Udv-ohuTOC5dDFqBE17a3L6pF9Tfn_lYQ_MHHkj8,86
|
|
11
|
+
span_panel_api_schema_0-1.0.0.dist-info/RECORD,,
|