span-panel-api-schema-0 1.2.0__tar.gz → 1.3.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_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/CHANGELOG.md +19 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/PKG-INFO +2 -2
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/pyproject.toml +4 -2
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/accumulator.py +3 -3
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/consumer.py +25 -54
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/.gitignore +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/README.md +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/__init__.py +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/adapter.py +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/const.py +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/field_metadata.py +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/py.typed +0 -0
- {span_panel_api_schema_0-1.2.0 → span_panel_api_schema_0-1.3.0b1}/src/span_panel_api_schema_0/reference/homie_schema.json +0 -0
|
@@ -9,6 +9,25 @@ rather than by this version number. A release here means this parser changed, ne
|
|
|
9
9
|
|
|
10
10
|
Pre-releases are not listed separately. A beta is a step towards the next public version, so its changes are folded into that version's entry as they land and are described against the last public release, never against the beta before it.
|
|
11
11
|
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
Requires `span-panel-api` **3.7.0 or newer**, whose snapshot fields accept the unknown values this parser now reports.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`proximity_proven` is `None`**, where it was `True` on every snapshot because it read readiness rather than any proximity property.
|
|
19
|
+
- **`uptime_s` is `None` before the panel first reports ready**, where it was `0`.
|
|
20
|
+
- **A circuit's `instant_power_update_time_s` and `energy_accum_update_time_s` are `None` until the reading arrives**, where they were `0`.
|
|
21
|
+
- **BREAKING FOR CONSUMERS: `HomiePropertyAccumulator.get_timestamp()` returns `int | None`, and `None` for a property never received**, where it returned `0`.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- **BREAKING: snapshots no longer include synthesised `unmapped_tab_N` entries for unoccupied breaker positions, which carried no measured data.**
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`uptime_s` keeps advancing while only circuits change**, where a snapshot rebuilt for changed circuits alone carried the previous snapshot's uptime forward until something panel-level changed.
|
|
30
|
+
|
|
12
31
|
## [1.2.0]
|
|
13
32
|
|
|
14
33
|
The panel's one inverter is reported through `pv_inverters` as well as `pv`. Requires `span-panel-api` **3.6.0 or newer**.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api-schema-0
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0b1
|
|
4
4
|
Summary: Flat-schema (data-model-version absent) parser 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
|
|
7
7
|
Author: SpanPanel
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
Requires-Python: <4.0,>=3.14
|
|
10
|
-
Requires-Dist: span-panel-api<4.0,>=3.
|
|
10
|
+
Requires-Dist: span-panel-api<4.0,>=3.7.0b1
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
|
|
13
13
|
# span-panel-api-schema-0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api-schema-0"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0b1"
|
|
4
4
|
description = "Flat-schema (data-model-version absent) parser for span-panel-api"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -19,7 +19,9 @@ dependencies = [
|
|
|
19
19
|
# Raised to 3.6.0, the first bootstrap whose snapshot carries `pv_inverters`
|
|
20
20
|
# and the inverter's `device_id` and `node_id`, which this parser fills.
|
|
21
21
|
# Against an earlier one, building a snapshot fails on those arguments.
|
|
22
|
-
|
|
22
|
+
# Raised to 3.7.0b1, whose snapshot fields accept the unknown values this
|
|
23
|
+
# parser now reports, where an earlier one types them as never unknown.
|
|
24
|
+
"span-panel-api>=3.7.0b1,<4.0",
|
|
23
25
|
]
|
|
24
26
|
|
|
25
27
|
[project.urls]
|
|
@@ -118,9 +118,9 @@ class HomiePropertyAccumulator:
|
|
|
118
118
|
"""Get a property's reported value."""
|
|
119
119
|
return self._property_values.get(node_id, {}).get(prop_id, default)
|
|
120
120
|
|
|
121
|
-
def get_timestamp(self, node_id: str, prop_id: str) -> int:
|
|
122
|
-
"""Get the epoch timestamp of a property's last update."""
|
|
123
|
-
return self._property_timestamps.get(node_id, {}).get(prop_id
|
|
121
|
+
def get_timestamp(self, node_id: str, prop_id: str) -> int | None:
|
|
122
|
+
"""Get the epoch timestamp of a property's last update, or None if it never arrived."""
|
|
123
|
+
return self._property_timestamps.get(node_id, {}).get(prop_id)
|
|
124
124
|
|
|
125
125
|
def get_target(self, node_id: str, prop_id: str) -> str | None:
|
|
126
126
|
"""Get a property's target value, or None if no target set."""
|
|
@@ -85,6 +85,16 @@ def _parse_int(value: str, default: int = 0) -> int:
|
|
|
85
85
|
return default
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _latest(*timestamps: int | None) -> int | None:
|
|
89
|
+
"""The most recent of the timestamps that exist, or `None` when none do.
|
|
90
|
+
|
|
91
|
+
A property that has not arrived has no receipt time, so it takes no part
|
|
92
|
+
rather than standing in as `0`.
|
|
93
|
+
"""
|
|
94
|
+
arrived = [timestamp for timestamp in timestamps if timestamp is not None]
|
|
95
|
+
return max(arrived) if arrived else None
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
class HomieDeviceConsumer:
|
|
89
99
|
"""Build SPAN-specific snapshots from accumulated Homie property state.
|
|
90
100
|
|
|
@@ -220,12 +230,8 @@ class HomieDeviceConsumer:
|
|
|
220
230
|
cached = self._cached_snapshot
|
|
221
231
|
|
|
222
232
|
feed_metadata = self._build_feed_metadata()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for cid, circ in cached.circuits.items():
|
|
226
|
-
if cid.startswith("unmapped_tab_"):
|
|
227
|
-
continue # drop old unmapped entries; will recompute below
|
|
228
|
-
updated_circuits[cid] = circ
|
|
233
|
+
# Keep cached circuits, rebuild dirty ones
|
|
234
|
+
updated_circuits = dict(cached.circuits)
|
|
229
235
|
for node_id in dirty:
|
|
230
236
|
if self.is_circuit_node(node_id):
|
|
231
237
|
meta = feed_metadata.get(node_id, {})
|
|
@@ -234,11 +240,14 @@ class HomieDeviceConsumer:
|
|
|
234
240
|
circuit = self._build_circuit(node_id, device_type, relative_position)
|
|
235
241
|
updated_circuits[circuit.circuit_id] = circuit
|
|
236
242
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
updated_circuits.
|
|
243
|
+
# Uptime is a clock, not a property, so no dirty node announces that it
|
|
244
|
+
# moved; carrying the cached value would freeze it between full builds.
|
|
245
|
+
return dataclasses.replace(cached, circuits=updated_circuits, uptime_s=self._uptime_s())
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
def _uptime_s(self) -> int | None:
|
|
248
|
+
"""Connection uptime since $state==ready, measured here; none before the first ready."""
|
|
249
|
+
ready_since = self._acc.ready_since
|
|
250
|
+
return int(time.monotonic() - ready_since) if ready_since > 0.0 else None
|
|
242
251
|
|
|
243
252
|
def _find_lugs_node(self, direction: str) -> str | None:
|
|
244
253
|
"""Find the lugs node with a specific direction.
|
|
@@ -339,8 +348,8 @@ class HomieDeviceConsumer:
|
|
|
339
348
|
|
|
340
349
|
always_on = _parse_bool(self._acc.get_prop(node_id, "always-on"))
|
|
341
350
|
|
|
342
|
-
# Timestamps from MQTT
|
|
343
|
-
energy_ts =
|
|
351
|
+
# Timestamps from MQTT receipt time, `None` until the reading arrives
|
|
352
|
+
energy_ts = _latest(
|
|
344
353
|
self._acc.get_timestamp(node_id, "exported-energy"),
|
|
345
354
|
self._acc.get_timestamp(node_id, "imported-energy"),
|
|
346
355
|
)
|
|
@@ -524,39 +533,6 @@ class HomieDeviceConsumer:
|
|
|
524
533
|
|
|
525
534
|
return "UNKNOWN"
|
|
526
535
|
|
|
527
|
-
def _build_unmapped_tabs(
|
|
528
|
-
self,
|
|
529
|
-
circuits: dict[str, SpanCircuitSnapshot],
|
|
530
|
-
) -> dict[str, SpanCircuitSnapshot]:
|
|
531
|
-
"""Synthesize unmapped tab entries for breaker positions with no circuit.
|
|
532
|
-
|
|
533
|
-
Creates zero-power SpanCircuitSnapshot entries for unoccupied positions
|
|
534
|
-
up to ``self._panel_size``.
|
|
535
|
-
"""
|
|
536
|
-
occupied_tabs: set[int] = set()
|
|
537
|
-
for circuit in circuits.values():
|
|
538
|
-
occupied_tabs.update(circuit.tabs)
|
|
539
|
-
|
|
540
|
-
unmapped: dict[str, SpanCircuitSnapshot] = {}
|
|
541
|
-
for tab in range(1, self._panel_size + 1):
|
|
542
|
-
if tab not in occupied_tabs:
|
|
543
|
-
circuit_id = f"unmapped_tab_{tab}"
|
|
544
|
-
unmapped[circuit_id] = SpanCircuitSnapshot(
|
|
545
|
-
circuit_id=circuit_id,
|
|
546
|
-
name=f"Unmapped Tab {tab}",
|
|
547
|
-
relay_state="CLOSED",
|
|
548
|
-
instant_power_w=0.0,
|
|
549
|
-
produced_energy_wh=0.0,
|
|
550
|
-
consumed_energy_wh=0.0,
|
|
551
|
-
tabs=[tab],
|
|
552
|
-
priority="UNKNOWN",
|
|
553
|
-
is_user_controllable=False,
|
|
554
|
-
is_sheddable=False,
|
|
555
|
-
is_never_backup=False,
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
return unmapped
|
|
559
|
-
|
|
560
536
|
def _build_snapshot(self) -> SpanPanelSnapshot:
|
|
561
537
|
"""Build full snapshot from accumulated property values."""
|
|
562
538
|
core_node = self._acc.find_node_by_type(TYPE_CORE)
|
|
@@ -670,10 +646,6 @@ class HomieDeviceConsumer:
|
|
|
670
646
|
circuit = self._build_circuit(node_id, device_type, relative_position)
|
|
671
647
|
circuits[circuit.circuit_id] = circuit
|
|
672
648
|
|
|
673
|
-
# Synthesize unmapped tab entries
|
|
674
|
-
unmapped = self._build_unmapped_tabs(circuits)
|
|
675
|
-
circuits.update(unmapped)
|
|
676
|
-
|
|
677
649
|
# Battery, PV, and EVSE metadata
|
|
678
650
|
battery = self._build_battery()
|
|
679
651
|
pv = self._build_pv()
|
|
@@ -690,9 +662,6 @@ class HomieDeviceConsumer:
|
|
|
690
662
|
dsm_state = self._derive_dsm_state(core_node, grid_power, power_flow_grid)
|
|
691
663
|
current_run_config = self._derive_run_config(dsm_state, grid_islandable, dominant_power_source)
|
|
692
664
|
|
|
693
|
-
# Connection uptime since $state==ready
|
|
694
|
-
uptime = int(time.monotonic() - self._acc.ready_since) if self._acc.ready_since > 0.0 else 0
|
|
695
|
-
|
|
696
665
|
return SpanPanelSnapshot(
|
|
697
666
|
serial_number=self._acc.serial_number,
|
|
698
667
|
firmware_version=firmware,
|
|
@@ -706,8 +675,10 @@ class HomieDeviceConsumer:
|
|
|
706
675
|
dsm_state=dsm_state,
|
|
707
676
|
current_run_config=current_run_config,
|
|
708
677
|
door_state=door_state,
|
|
709
|
-
|
|
710
|
-
|
|
678
|
+
# No flat property reports proximity. Readiness is not a stand-in:
|
|
679
|
+
# a snapshot is only built once ready, so it would always say True.
|
|
680
|
+
proximity_proven=None,
|
|
681
|
+
uptime_s=self._uptime_s(),
|
|
711
682
|
eth0_link=eth0,
|
|
712
683
|
wlan_link=wlan,
|
|
713
684
|
wwan_link=wwan_connected,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|