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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [span_panel_api.schema_adapters]
2
+ schema_0 = span_panel_api_schema_0:SchemaZeroAdapter