span-panel-api-schema-1 1.4.0b2__tar.gz → 1.4.0b3__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_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/.gitignore +1 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/CHANGELOG.md +2 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/PKG-INFO +1 -1
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/pyproject.toml +1 -1
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/adapter.py +29 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/transport.py +45 -17
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/README.md +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/breaker.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/charge-limit.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/connection.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/door.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/grid-forming.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/grid.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/info.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/load-shed.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/meter.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/pcs.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/power-flows.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/shed-forecast.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/shed.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/soc.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/status.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/switch.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/registries/device-types.md +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/__init__.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/adoption.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/catalog.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/charge_limit.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/circuits.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/const.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/description.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/devices.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/extension.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/field_metadata.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/firmware.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/panel.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/py.typed +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/reference/parent_child_tree.json +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/snapshot.py +0 -0
- {span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/src/span_panel_api_schema_1/spec_lock.json +0 -0
|
@@ -35,6 +35,8 @@ Requires `span-panel-api` **3.8.0 or newer**, whose snapshot fields accept the u
|
|
|
35
35
|
- **The grid-forming device name falls back to vendor and model instead of showing a device id.**
|
|
36
36
|
- **A panel with no MID reports `dsm_state` `DSM_ON_GRID` and `current_run_config` `PANEL_ON_GRID` whenever it is publishing**, where both dropped to `UNKNOWN` each time `power-flows/grid` read within 1 W of zero
|
|
37
37
|
([#215](https://github.com/SpanPanel/span-panel-api/issues/215)).
|
|
38
|
+
- **Each MQTT message costs the same few microseconds however many devices the panel declares**, where routing it tested every recorded subscription in turn and every readiness check walked the device tree. On a 30-device tree the work per message falls
|
|
39
|
+
from 90–250 µs to about 2 µs, measured on a desktop machine ([#223](https://github.com/SpanPanel/span-panel-api/issues/223)).
|
|
38
40
|
|
|
39
41
|
## [1.2.2]
|
|
40
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api-schema-1
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.0b3
|
|
4
4
|
Summary: Parent/child schema (data-model-version 1.x) 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
|
|
@@ -118,7 +118,14 @@ class SchemaOneAdapter:
|
|
|
118
118
|
self._controller = Controller(root_device_id=serial_number, mqttc=self._routes)
|
|
119
119
|
self._property_callbacks: list[Callable[[str, str, str, str | None], None]] = []
|
|
120
120
|
self._awaiting: tuple[str, ...] = ()
|
|
121
|
+
# Readiness as last computed, or None once the tree has changed since.
|
|
122
|
+
self._ready: bool | None = None
|
|
121
123
|
self._controller.set_on_property_changed_callback(self._on_property_changed)
|
|
124
|
+
# Every event that can change what `is_ready()` answers; see there.
|
|
125
|
+
self._controller.set_on_device_discovered_callback(self._tree_changed)
|
|
126
|
+
self._controller.set_on_device_state_changed_callback(self._tree_changed)
|
|
127
|
+
self._controller.set_on_device_removed_callback(self._tree_changed)
|
|
128
|
+
self._controller.set_on_description_received_callback(self._tree_changed)
|
|
122
129
|
# Records the subscriptions the tree walk needs; nothing reaches the
|
|
123
130
|
# wire, because this object has no connection to reach it with.
|
|
124
131
|
self._controller.start_discovery()
|
|
@@ -173,7 +180,22 @@ class SchemaOneAdapter:
|
|
|
173
180
|
panel's positions with none to lay out. Asking only for what the panel
|
|
174
181
|
itself promised keeps a firmware that omits the property connectable —
|
|
175
182
|
it falls back to the drift warning in `panel_size_from_model`.
|
|
183
|
+
|
|
184
|
+
**The answer is remembered until the tree changes.** The transport asks
|
|
185
|
+
before and after every message, and walking the tree each time cost
|
|
186
|
+
more than routing the message did (#223). Only the Controller's own
|
|
187
|
+
events can move it: a device's first `$state`, a change of state, a
|
|
188
|
+
removal, a `$description`, and the root's `info/model` value. Each
|
|
189
|
+
forgets the answer and the next call works it out again. The events
|
|
190
|
+
never work it out themselves, because a description is announced before
|
|
191
|
+
the SDK has reconciled the children it declares.
|
|
176
192
|
"""
|
|
193
|
+
if self._ready is None:
|
|
194
|
+
self._ready = self._tree_ready()
|
|
195
|
+
return self._ready
|
|
196
|
+
|
|
197
|
+
def _tree_ready(self) -> bool:
|
|
198
|
+
"""What `is_ready()` answers, worked out from the tree as it stands."""
|
|
177
199
|
root = self._controller.get_root(self._serial_number)
|
|
178
200
|
if root is None or root.state != STATE_READY or not root.description:
|
|
179
201
|
return False
|
|
@@ -626,5 +648,12 @@ class SchemaOneAdapter:
|
|
|
626
648
|
protocol has no place for the previous value, and consumers that need
|
|
627
649
|
one keep it themselves.
|
|
628
650
|
"""
|
|
651
|
+
# The one value readiness waits on.
|
|
652
|
+
if (device_id, node_id, property_id) == (self._serial_number, NODE_INFO, PROP_MODEL):
|
|
653
|
+
self._ready = None
|
|
629
654
|
for callback in list(self._property_callbacks):
|
|
630
655
|
callback(device_id, node_id, property_id, value)
|
|
656
|
+
|
|
657
|
+
def _tree_changed(self, *_event: object) -> None:
|
|
658
|
+
"""Forget the remembered readiness. Accepts any of the Controller's tree events."""
|
|
659
|
+
self._ready = None
|
|
@@ -38,10 +38,11 @@ zero circuits.
|
|
|
38
38
|
|
|
39
39
|
from __future__ import annotations
|
|
40
40
|
|
|
41
|
+
import itertools
|
|
41
42
|
import logging
|
|
42
|
-
from typing import TYPE_CHECKING, Any
|
|
43
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
43
44
|
|
|
44
|
-
from paho.mqtt.
|
|
45
|
+
from paho.mqtt.matcher import MQTTMatcher
|
|
45
46
|
|
|
46
47
|
from span_panel_api.models import ControlTarget
|
|
47
48
|
from span_panel_api_schema_1.const import HOMIE_DOMAIN, HOMIE_VERSION
|
|
@@ -80,6 +81,13 @@ def control_target(device_id: str, node: str, prop: str) -> ControlTarget:
|
|
|
80
81
|
MAX_HELD_MESSAGES = 4096
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
class _Route(NamedTuple):
|
|
85
|
+
"""A recorded callback, and when it was recorded, which decides an overlap."""
|
|
86
|
+
|
|
87
|
+
recorded: int
|
|
88
|
+
callback: Callable[[str, bytes], None]
|
|
89
|
+
|
|
90
|
+
|
|
83
91
|
class ControllerRoutes:
|
|
84
92
|
"""Record `Controller`'s subscriptions and route messages back to them.
|
|
85
93
|
|
|
@@ -88,8 +96,11 @@ class ControllerRoutes:
|
|
|
88
96
|
"""
|
|
89
97
|
|
|
90
98
|
def __init__(self) -> None:
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
self._routes: dict[str, _Route] = {}
|
|
100
|
+
# The same patterns, indexed for matching: each maps to itself, and
|
|
101
|
+
# dispatch resolves the patterns that match through `_routes`.
|
|
102
|
+
self._matcher = MQTTMatcher()
|
|
103
|
+
self._recordings = itertools.count()
|
|
93
104
|
# Last payload per topic that matched no route yet, keyed by topic so a
|
|
94
105
|
# newer value supersedes an older one — the same last-value-wins rule
|
|
95
106
|
# the broker applies to the retained message this stands in for.
|
|
@@ -132,27 +143,35 @@ class ControllerRoutes:
|
|
|
132
143
|
the adapter's behalf, not of a route recorded in a dict.
|
|
133
144
|
"""
|
|
134
145
|
# Pop before insert: assigning an existing key updates the value but
|
|
135
|
-
# keeps the key's original position,
|
|
136
|
-
# pattern behind whatever was added after it in dispatch's match order.
|
|
146
|
+
# keeps the key's original position, and `routes` lists most recent last.
|
|
137
147
|
self._routes.pop(sub, None)
|
|
138
|
-
self._routes[sub] = param
|
|
148
|
+
self._routes[sub] = _Route(next(self._recordings), param)
|
|
149
|
+
self._matcher[sub] = sub
|
|
139
150
|
self._release(sub, param)
|
|
140
151
|
|
|
141
152
|
def unsubscribe(self, sub: str) -> None:
|
|
142
153
|
"""Forget the callback for `sub`.
|
|
143
154
|
|
|
144
155
|
The broad wire subscription stays. Messages for a device the SDK has
|
|
145
|
-
dropped simply stop matching a route, and dispatch
|
|
156
|
+
dropped simply stop matching a route, and dispatch holds them.
|
|
146
157
|
"""
|
|
147
|
-
|
|
158
|
+
# Guarded because the matcher raises for a pattern it never held, and
|
|
159
|
+
# forgetting one that was never recorded is harmless.
|
|
160
|
+
if self._routes.pop(sub, None) is not None:
|
|
161
|
+
del self._matcher[sub]
|
|
148
162
|
|
|
149
163
|
# -- our side ----------------------------------------------------------
|
|
150
164
|
|
|
151
165
|
def dispatch(self, topic: str, payload: str) -> None:
|
|
152
166
|
"""Deliver one message to the callback of the route that matches.
|
|
153
167
|
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
Matched through a prefix tree of the recorded patterns, so the cost
|
|
169
|
+
follows the topic's depth rather than the number of routes. Every
|
|
170
|
+
message takes this path, and a linear scan grew with each device the
|
|
171
|
+
panel declares, at four routes per device (#223).
|
|
172
|
+
|
|
173
|
+
The most recently recorded match wins, which is defensive rather than
|
|
174
|
+
currently required. Tree-rooted discovery subscribes four **device-scoped**
|
|
156
175
|
patterns per device — `$state`, `$description`, `+/+`, `+/+/$target`
|
|
157
176
|
(`Controller._subscribe_device_topics`) — which cannot overlap each
|
|
158
177
|
other or another device's, so today exactly one route matches any topic.
|
|
@@ -172,11 +191,15 @@ class ControllerRoutes:
|
|
|
172
191
|
|
|
173
192
|
The SDK hands callbacks `bytes`; the transport hands us `str`.
|
|
174
193
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
self.
|
|
194
|
+
# The matcher is untyped and yields what it was given, the pattern
|
|
195
|
+
# string; `_routes` turns each back into a typed route. The list is
|
|
196
|
+
# complete before any callback runs, so a callback that subscribes
|
|
197
|
+
# changes a matcher nothing is still walking.
|
|
198
|
+
matched = [self._routes[sub] for sub in self._matcher.iter_match(topic)]
|
|
199
|
+
if not matched:
|
|
200
|
+
self._hold(topic, payload)
|
|
201
|
+
return
|
|
202
|
+
max(matched, key=lambda route: route.recorded).callback(topic, payload.encode())
|
|
180
203
|
|
|
181
204
|
def _hold(self, topic: str, payload: str) -> None:
|
|
182
205
|
"""Keep a message for a route that has not been registered yet."""
|
|
@@ -201,8 +224,13 @@ class ControllerRoutes:
|
|
|
201
224
|
in turn, so a whole tree unfolds from one root subscription. The
|
|
202
225
|
candidate list is therefore taken up front and each entry re-checked,
|
|
203
226
|
since a nested release may have claimed it already.
|
|
227
|
+
|
|
228
|
+
The pattern is indexed once and matched against every held topic,
|
|
229
|
+
rather than indexed afresh for each of them.
|
|
204
230
|
"""
|
|
205
|
-
|
|
231
|
+
pattern = MQTTMatcher()
|
|
232
|
+
pattern[sub] = sub
|
|
233
|
+
for topic in [held for held in self._held if next(pattern.iter_match(held), None) is not None]:
|
|
206
234
|
payload = self._held.pop(topic, None)
|
|
207
235
|
if payload is not None:
|
|
208
236
|
callback(topic, payload.encode())
|
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/breaker.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/charge-limit.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/connection.json
RENAMED
|
File without changes
|
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/grid-forming.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/load-shed.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/meter.json
RENAMED
|
File without changes
|
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/power-flows.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/shed-forecast.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/status.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/catalogs/switch.json
RENAMED
|
File without changes
|
{span_panel_api_schema_1-1.4.0b2 → span_panel_api_schema_1-1.4.0b3}/spec/registries/device-types.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|