span-panel-api-schema-1 1.1.2__tar.gz → 1.1.4__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.1.2 → span_panel_api_schema_1-1.1.4}/.gitignore +0 -5
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/CHANGELOG.md +24 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/PKG-INFO +19 -7
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/README.md +18 -6
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/pyproject.toml +1 -1
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/adapter.py +22 -6
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/field_metadata.py +2 -2
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/panel.py +14 -12
- span_panel_api_schema_1-1.1.4/src/span_panel_api_schema_1/reference/parent_child_tree.json +252 -0
- span_panel_api_schema_1-1.1.4/src/span_panel_api_schema_1/spec_lock.json +42 -0
- span_panel_api_schema_1-1.1.2/spec/fixtures/simulator_tree.json +0 -4853
- span_panel_api_schema_1-1.1.2/spec/fixtures/simulator_wire.json +0 -684
- span_panel_api_schema_1-1.1.2/src/span_panel_api_schema_1/spec_lock.json +0 -72
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/breaker.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/charge-limit.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/connection.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/door.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/grid-forming.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/grid.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/info.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/load-shed.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/meter.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/pcs.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/power-flows.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/shed-forecast.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/shed.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/soc.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/status.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/catalogs/switch.json +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/spec/registries/device-types.md +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/__init__.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/adoption.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/catalog.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/charge_limit.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/circuits.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/const.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/description.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/devices.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/extension.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/py.typed +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/snapshot.py +0 -0
- {span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/transport.py +0 -0
|
@@ -40,8 +40,3 @@ coverage_output.log
|
|
|
40
40
|
# of which belongs in a repository. The differential that reads them commits its
|
|
41
41
|
# *verdict* only, never the capture, and skips when the file is absent.
|
|
42
42
|
tests/fixtures/live_*.json
|
|
43
|
-
|
|
44
|
-
# Peer checkouts. CI clones the eBus specification and SpanPanel/panelbench here so
|
|
45
|
-
# the provenance checks have something to compare vendored bytes against; the same
|
|
46
|
-
# layout works locally if you would rather not point .env at siblings.
|
|
47
|
-
/peers/
|
|
@@ -9,6 +9,30 @@ number. A release here means this parser changed, never that the panel did.
|
|
|
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
|
+
## [1.1.4]
|
|
13
|
+
|
|
14
|
+
The parser reports the islanding state the panel is acting on, and refuses islanding writes the panel would ignore. Reported and fixed by [@dcj](https://github.com/dcj) in [#179](https://github.com/SpanPanel/span-panel-api/pull/179), from issue
|
|
15
|
+
[#178](https://github.com/SpanPanel/span-panel-api/issues/178).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **An islanding assertion in force is now the reported islanding state even while the MID is ready.** `resolve_islanding_state` returned the MID's sensed value whenever the MID was ready and read `shed/asserted-islanding-state` only when it was not, so
|
|
20
|
+
`dsm_state` could disagree with the state the panel was acting on. `ON_GRID` or `OFF_GRID` asserted now wins; otherwise the ready MID's value, then the existing fallback.
|
|
21
|
+
- **`set_dominant_power_source_target` returns None unless the panel declares `shed/asserted-islanding-state` settable**, the same refusal `set_circuit_relay_target` makes.
|
|
22
|
+
- **`dominant_power_source_payload` refuses `NONE` and `UNKNOWN`** instead of publishing `NONE`, which the panel ignores. An assertion clears itself once the panel's link to the battery recovers.
|
|
23
|
+
|
|
24
|
+
## [1.1.3]
|
|
25
|
+
|
|
26
|
+
`spec_lock.json` ships inside this wheel, so its shape changing is a release here even though the parser did not change.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`span_panel_api_schema_1/reference/parent_child_tree.json`** — the reference capture, shipped as package data so a downstream test suite replays the bytes its pinned version was tested against instead of vendoring a copy.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **`spec_lock.json` loses its `peers` block** — the producer this parser is developed against is now an ordinary pinned dev dependency of the repository rather than a peer recorded in the lockfile.
|
|
35
|
+
|
|
12
36
|
## [1.1.2]
|
|
13
37
|
|
|
14
38
|
A refreshed peer pin: `spec_lock.json` ships inside this wheel, so the reference capture's producer moving is a release here even though the parser did not change.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api-schema-1
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
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
|
|
@@ -54,16 +54,28 @@ span-panel-api's transport covers the whole tree and each message is routed to w
|
|
|
54
54
|
|
|
55
55
|
## Conformance
|
|
56
56
|
|
|
57
|
-
`spec_lock.json` ships with the package and
|
|
58
|
-
addresses are byte-copied under `spec
|
|
57
|
+
`spec_lock.json` ships with the package and is this parser's declaration as a consumer: the firmware range it reads, the eBus specification commit its vocabulary was read from, and the version of every capability, device and registry it implements. The
|
|
58
|
+
capability catalogs it addresses are byte-copied into the repository under `spec/`, and the repository checks them against the copies the eBus emitter carries in its own wheel.
|
|
59
59
|
|
|
60
60
|
Those copies exist to be **checked against, never parsed in production** — units and datatypes come from each device's `$description`, since a catalog is the superset across all hardware rather than a statement about the panel in front of you. The suite
|
|
61
61
|
asks the consumer's question rather than the publisher's: is every name this adapter _reads_ one the specification defines? A consumer addressing a name that no longer exists does not fail loudly, it goes quiet — the property never arrives, metadata lookup
|
|
62
62
|
returns `None`, and an entity disappears.
|
|
63
63
|
|
|
64
|
-
## Reference
|
|
64
|
+
## Reference payload
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
`span_panel_api_schema_1/reference/parent_child_tree.json` ships in this wheel: a retained-topic capture of a full 40-space parent/child panel — 14 devices, `{device_id: {topic: payload}}`, every value a string exactly as a broker retains it.
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
**Test-support data, and no runtime path reads it.** It ships so a downstream test suite pinned to a version of this adapter replays the bytes that version was built and tested against, out of its own site-packages:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from importlib.resources import files
|
|
72
|
+
import json
|
|
73
|
+
|
|
74
|
+
tree = json.loads((files("span_panel_api_schema_1") / "reference" / "parent_child_tree.json").read_text(encoding="utf-8"))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Vendoring a copy instead means also maintaining a guard to keep the copy honest, which is what this replaces. It was package data until 1.1.0, a fixture of the repository's test suite for 1.1.2, and package data again from 1.1.3 — for the cost, not for the
|
|
78
|
+
principle: no runtime path has ever read it.
|
|
79
|
+
|
|
80
|
+
The bytes are produced by `ebus-panel-sim`, pinned in the repository's dev dependencies, and the repository's suite regenerates the capture in-process on every run and compares it, so a tree the pinned producer does not reproduce is a test failure rather
|
|
81
|
+
than a claim in a document.
|
|
@@ -41,16 +41,28 @@ span-panel-api's transport covers the whole tree and each message is routed to w
|
|
|
41
41
|
|
|
42
42
|
## Conformance
|
|
43
43
|
|
|
44
|
-
`spec_lock.json` ships with the package and
|
|
45
|
-
addresses are byte-copied under `spec
|
|
44
|
+
`spec_lock.json` ships with the package and is this parser's declaration as a consumer: the firmware range it reads, the eBus specification commit its vocabulary was read from, and the version of every capability, device and registry it implements. The
|
|
45
|
+
capability catalogs it addresses are byte-copied into the repository under `spec/`, and the repository checks them against the copies the eBus emitter carries in its own wheel.
|
|
46
46
|
|
|
47
47
|
Those copies exist to be **checked against, never parsed in production** — units and datatypes come from each device's `$description`, since a catalog is the superset across all hardware rather than a statement about the panel in front of you. The suite
|
|
48
48
|
asks the consumer's question rather than the publisher's: is every name this adapter _reads_ one the specification defines? A consumer addressing a name that no longer exists does not fail loudly, it goes quiet — the property never arrives, metadata lookup
|
|
49
49
|
returns `None`, and an entity disappears.
|
|
50
50
|
|
|
51
|
-
## Reference
|
|
51
|
+
## Reference payload
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
`span_panel_api_schema_1/reference/parent_child_tree.json` ships in this wheel: a retained-topic capture of a full 40-space parent/child panel — 14 devices, `{device_id: {topic: payload}}`, every value a string exactly as a broker retains it.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
**Test-support data, and no runtime path reads it.** It ships so a downstream test suite pinned to a version of this adapter replays the bytes that version was built and tested against, out of its own site-packages:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from importlib.resources import files
|
|
59
|
+
import json
|
|
60
|
+
|
|
61
|
+
tree = json.loads((files("span_panel_api_schema_1") / "reference" / "parent_child_tree.json").read_text(encoding="utf-8"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Vendoring a copy instead means also maintaining a guard to keep the copy honest, which is what this replaces. It was package data until 1.1.0, a fixture of the repository's test suite for 1.1.2, and package data again from 1.1.3 — for the cost, not for the
|
|
65
|
+
principle: no runtime path has ever read it.
|
|
66
|
+
|
|
67
|
+
The bytes are produced by `ebus-panel-sim`, pinned in the repository's dev dependencies, and the repository's suite regenerates the capture in-process on every run and compares it, so a tree the pinned producer does not reproduce is a test failure rather
|
|
68
|
+
than a claim in a document.
|
|
@@ -40,7 +40,7 @@ from span_panel_api_schema_1.const import (
|
|
|
40
40
|
STATE_READY,
|
|
41
41
|
TYPE_CIRCUIT,
|
|
42
42
|
)
|
|
43
|
-
from span_panel_api_schema_1.description import device_type
|
|
43
|
+
from span_panel_api_schema_1.description import declared_settable, device_type, node_properties
|
|
44
44
|
from span_panel_api_schema_1.field_metadata import build_field_metadata
|
|
45
45
|
from span_panel_api_schema_1.panel import integer
|
|
46
46
|
from span_panel_api_schema_1.snapshot import TreeRoles, build_snapshot, harmonised_evse_keys
|
|
@@ -275,9 +275,23 @@ class SchemaOneAdapter:
|
|
|
275
275
|
the BESS stops discharging. Returning None here, as this did until the
|
|
276
276
|
successor was decided, left that recovery unavailable during an outage.
|
|
277
277
|
|
|
278
|
+
**The panel decides whether a write takes effect, by its own link to the
|
|
279
|
+
battery.** It accepts `ON_GRID` or `OFF_GRID` only while the battery's
|
|
280
|
+
`status/communication-state` is not `OK`; otherwise it ignores the write
|
|
281
|
+
and the published value does not change. A lost MID alone does not make
|
|
282
|
+
the write eligible. `NONE` is always ignored.
|
|
283
|
+
|
|
284
|
+
**None unless the panel declares the assertion `$settable`**, the same
|
|
285
|
+
refusal `set_circuit_relay_target` makes. Absence of the declaration,
|
|
286
|
+
or of the property, authorizes nothing (see
|
|
287
|
+
`description.declared_settable`).
|
|
288
|
+
|
|
278
289
|
Payload translation is not optional: the flat enum this protocol speaks
|
|
279
290
|
is not the one the panel accepts. See `dominant_power_source_payload`.
|
|
280
291
|
"""
|
|
292
|
+
root = self._controller.get_root(self._serial_number)
|
|
293
|
+
if not declared_settable(node_properties(root, NODE_SHED).get(PROP_ASSERTED_ISLANDING_STATE)):
|
|
294
|
+
return None
|
|
281
295
|
return self._target(self._serial_number, NODE_SHED, PROP_ASSERTED_ISLANDING_STATE)
|
|
282
296
|
|
|
283
297
|
def dominant_power_source_payload(self, value: str) -> str | None:
|
|
@@ -291,17 +305,19 @@ class SchemaOneAdapter:
|
|
|
291
305
|
|
|
292
306
|
The narrowing loses nothing, because the six values were a *source
|
|
293
307
|
class* pressed into service as a manual override and the job only ever
|
|
294
|
-
needed on-grid
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
needed on-grid or off-grid. Anything not recognized returns None rather
|
|
309
|
+
than guessing, so the transport refuses the command instead of
|
|
310
|
+
asserting something the user did not ask for.
|
|
311
|
+
|
|
312
|
+
**`NONE` and `UNKNOWN` are refused too.** The panel ignores a written
|
|
313
|
+
`NONE`, so publishing one would report a clear that never happened. An
|
|
314
|
+
assertion clears itself once the panel's link to the battery recovers.
|
|
297
315
|
"""
|
|
298
316
|
return {
|
|
299
317
|
"GRID": "ON_GRID",
|
|
300
318
|
"BATTERY": "OFF_GRID",
|
|
301
319
|
"PV": "OFF_GRID",
|
|
302
320
|
"GENERATOR": "OFF_GRID",
|
|
303
|
-
"NONE": "NONE",
|
|
304
|
-
"UNKNOWN": "NONE",
|
|
305
321
|
}.get(value.strip().upper())
|
|
306
322
|
|
|
307
323
|
def set_evse_charge_limit_target(self, node_id: str) -> ControlTarget | None:
|
|
@@ -472,8 +472,8 @@ _CONSUMED_OFF_SNAPSHOT: dict[tuple[str, str, str], str] = {
|
|
|
472
472
|
"parses the tree, so it is consumed before any snapshot exists"
|
|
473
473
|
),
|
|
474
474
|
(TYPE_PANEL, NODE_SHED, "asserted-islanding-state"): (
|
|
475
|
-
"tier
|
|
476
|
-
"
|
|
475
|
+
"tier 1 of resolve_islanding_state (panel.py), which returns a non-NONE "
|
|
476
|
+
"assertion first, and the write target of set_dominant_power_source_target"
|
|
477
477
|
),
|
|
478
478
|
(TYPE_LUGS, NODE_CONNECTION, "feeds-device-id"): (
|
|
479
479
|
"the downstream-lugs feedthrough branch of resolve_relative_position "
|
{span_panel_api_schema_1-1.1.2 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/panel.py
RENAMED
|
@@ -600,7 +600,6 @@ _GRID_POWER_EPSILON_W = 1.0
|
|
|
600
600
|
|
|
601
601
|
ISLANDING_ON_GRID = "ON_GRID"
|
|
602
602
|
ISLANDING_OFF_GRID = "OFF_GRID"
|
|
603
|
-
ASSERTION_NONE = "NONE"
|
|
604
603
|
|
|
605
604
|
|
|
606
605
|
def resolve_islanding_state(mid: DiscoveredDevice | None, panel: DiscoveredDevice) -> str | None:
|
|
@@ -608,17 +607,19 @@ def resolve_islanding_state(mid: DiscoveredDevice | None, panel: DiscoveredDevic
|
|
|
608
607
|
|
|
609
608
|
| tier | condition | source |
|
|
610
609
|
| --- | --- | --- |
|
|
611
|
-
| 1 |
|
|
612
|
-
| 2 | MID
|
|
610
|
+
| 1 | `shed/asserted-islanding-state` is `ON_GRID` or `OFF_GRID` | asserted |
|
|
611
|
+
| 2 | MID `$state` is `ready` and `islanding-state` present | sensed |
|
|
613
612
|
| 3 | no MID at all | `power-flows/grid` heuristic |
|
|
614
613
|
| 4 | none of the above | unknown |
|
|
615
614
|
|
|
616
|
-
**
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
615
|
+
**An assertion in force is the effective state, so it outranks the MID.** When the
|
|
616
|
+
panel's link to the battery is not healthy (the battery's `status/communication-state`
|
|
617
|
+
is not `OK`) and the grid returns, the user asserts the grid is up so the BESS stops
|
|
618
|
+
discharging, and the panel acts on that assertion until it clears. The panel ignores
|
|
619
|
+
the write while that link is healthy.
|
|
620
|
+
Reading the MID first would report the sensed value while the panel is acting on a
|
|
621
|
+
different one. The assertion clears itself back to `NONE` once the battery link
|
|
622
|
+
recovers, so the MID's answer returns without anything here having to expire it.
|
|
622
623
|
|
|
623
624
|
**Tier 3 never answers `OFF_GRID`, and never asserts on-grid from a missing MID.** An
|
|
624
625
|
earlier draft reasoned that no MID means no islanding authority means on-grid. That is
|
|
@@ -627,14 +628,15 @@ def resolve_islanding_state(mid: DiscoveredDevice | None, panel: DiscoveredDevic
|
|
|
627
628
|
counterexample. Grid power flowing is positive evidence of being on-grid; its absence
|
|
628
629
|
is not evidence of the opposite.
|
|
629
630
|
"""
|
|
631
|
+
asserted = text(panel, NODE_SHED, PROP_ASSERTED_ISLANDING_STATE)
|
|
632
|
+
if asserted in (ISLANDING_ON_GRID, ISLANDING_OFF_GRID):
|
|
633
|
+
return asserted
|
|
634
|
+
|
|
630
635
|
if mid is not None:
|
|
631
636
|
if mid.state == "ready":
|
|
632
637
|
sensed = text(mid, NODE_GRID, PROP_ISLANDING_STATE)
|
|
633
638
|
if sensed:
|
|
634
639
|
return sensed
|
|
635
|
-
asserted = text(panel, NODE_SHED, PROP_ASSERTED_ISLANDING_STATE)
|
|
636
|
-
if asserted and asserted != ASSERTION_NONE:
|
|
637
|
-
return asserted
|
|
638
640
|
return None
|
|
639
641
|
|
|
640
642
|
grid_power = number(panel, NODE_POWER_FLOWS, "grid")
|