span-panel-api-schema-1 1.2.0__tar.gz → 1.2.1__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.
Files changed (40) hide show
  1. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/CHANGELOG.md +22 -11
  2. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/PKG-INFO +2 -2
  3. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/pyproject.toml +3 -2
  4. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/circuits.py +2 -2
  5. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/snapshot.py +37 -32
  6. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/.gitignore +0 -0
  7. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/README.md +0 -0
  8. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/breaker.json +0 -0
  9. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/charge-limit.json +0 -0
  10. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/connection.json +0 -0
  11. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/door.json +0 -0
  12. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/grid-forming.json +0 -0
  13. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/grid.json +0 -0
  14. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/info.json +0 -0
  15. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/load-shed.json +0 -0
  16. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/meter.json +0 -0
  17. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/pcs.json +0 -0
  18. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/power-flows.json +0 -0
  19. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/shed-forecast.json +0 -0
  20. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/shed.json +0 -0
  21. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/soc.json +0 -0
  22. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/status.json +0 -0
  23. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/catalogs/switch.json +0 -0
  24. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/spec/registries/device-types.md +0 -0
  25. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/__init__.py +0 -0
  26. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/adapter.py +0 -0
  27. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/adoption.py +0 -0
  28. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/catalog.py +0 -0
  29. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/charge_limit.py +0 -0
  30. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/const.py +0 -0
  31. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/description.py +0 -0
  32. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/devices.py +0 -0
  33. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/extension.py +0 -0
  34. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/field_metadata.py +0 -0
  35. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/firmware.py +0 -0
  36. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/panel.py +0 -0
  37. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/py.typed +0 -0
  38. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/reference/parent_child_tree.json +0 -0
  39. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/spec_lock.json +0 -0
  40. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1}/src/span_panel_api_schema_1/transport.py +0 -0
@@ -9,26 +9,37 @@ 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.2.0]
12
+ ## [1.2.1]
13
13
 
14
- Every PV inverter a panel commissions is read, and the battery's power keeps its sign on firmware r202639. Requires `span-panel-api` **3.6.0 or newer**.
14
+ Every PV inverter a panel commissions is read, and the battery's power keeps its sign on firmware r202639. Requires `span-panel-api` **3.6.1 or newer**.
15
+
16
+ ### Added
17
+
18
+ - **Every PV inverter is read, not only the first**, each landing in `pv_inverters` with its serial number when published, its device id and its key.
19
+ - **`pv.serial_number` is read from `info/serial-number`**, so that property no longer appears as a discovery row.
20
+ - **`TreeRoles.pvs` and `devices.pv_inverter_key`** list every inverter and give an inverter's key.
21
+ - **`span_panel_api_schema_1.firmware.release_build`** reads the six-digit release build number out of a firmware version string, or returns `None` when it has none.
22
+ - **`build_battery` takes keyword-only `firmware_version` and `power_flow_battery`** to pick the BESS meter's wire frame, and `SchemaOneAdapter` a keyword-only `clock` for the wait on feeding circuits, and both behave as before without them.
23
+
24
+ ### Changed
25
+
26
+ - **`pv` describes the inverters together when a panel commissions more than one**, with their shared vendor and model, the sum of their installed DC sizes and a link that is down if any is down, rather than whichever came first in the device tree.
27
+
28
+ ### Removed
29
+
30
+ - **`TreeRoles.pv`**, which held only the first inverter, replaced by `TreeRoles.pvs`.
15
31
 
16
32
  ### Fixed
17
33
 
18
- - **A second inverter's feeding circuit is labeled `device_type="pv"`**, so its power no longer reads as a load with the opposite sign.
19
- - **A PV inverter wired through two breakers now reports its power and link status consistently**, with both circuits labeled `pv` and its feed and link status read from the one with the lowest circuit id.
20
- - **`pv` is the inverter on the lowest breaker space when more than one is commissioned**, rather than whichever came first in the device tree.
34
+ - **Every inverter's feeding circuit is labeled `device_type="pv"`**, so a second inverter's power no longer reads as a load with the opposite sign.
35
+ - **A PV inverter or charger wired through two breakers reports its power and link status consistently**, with both circuits labeled for it and its feed and link status read from the one with the lowest circuit id.
21
36
  - **Each inverter's extension properties are filed under that inverter**, with its `pv_inverters` key as the `instance_key` when more than one is commissioned.
22
37
  - **The first snapshot waits briefly for the circuits that feed an inverter**, so an inverter's key and its circuit's label do not change just after connecting.
23
38
  - **`battery.power_w` is discharge-positive on firmware r202639 and later, from the first snapshot on**, where a charging battery read positive.
24
39
 
25
- ### Added
40
+ ## [1.2.0] [YANKED]
26
41
 
27
- - **`span_panel_api_schema_1.firmware.release_build`** reads the six-digit release build number out of a firmware version string, or returns `None` when it has none.
28
- - **`build_battery` takes keyword-only `firmware_version` and `power_flow_battery`** to pick the BESS meter's wire frame, and behaves as before without them.
29
- - **Every PV inverter is read, not only the first**, each landing in `pv_inverters` with its serial number when published, its device id and its key.
30
- - **`TreeRoles.pvs` lists every inverter**, and `snapshot.primary_pv`, `devices.pv_inverter_key` and `circuits.circuit_tabs` choose the one `pv` describes, give an inverter's key, and read a circuit's breaker spaces.
31
- - **`pv.serial_number` is read from `info/serial-number`**, so that property no longer appears as a discovery row.
42
+ Withdrawn from PyPI; 1.2.1 replaces it.
32
43
 
33
44
  ## [1.1.4]
34
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: span-panel-api-schema-1
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -8,7 +8,7 @@ Author: SpanPanel
8
8
  License-Expression: MIT
9
9
  Requires-Python: <4.0,>=3.14
10
10
  Requires-Dist: ebus-sdk<0.24,>=0.19.0
11
- Requires-Dist: span-panel-api<4.0,>=3.6.0
11
+ Requires-Dist: span-panel-api<4.0,>=3.6.1
12
12
  Description-Content-Type: text/markdown
13
13
 
14
14
  # span-panel-api-schema-1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api-schema-1"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -25,7 +25,8 @@ dependencies = [
25
25
  # and the inverter's `serial_number`, `device_id` and `node_id`, which this
26
26
  # parser fills. Against an earlier one, building a snapshot fails on those
27
27
  # arguments.
28
- "span-panel-api>=3.6.0,<4.0",
28
+ # Raised to 3.6.1, whose SpanPanelSnapshot.pv documents what this parser builds.
29
+ "span-panel-api>=3.6.1,<4.0",
29
30
  # Only this distribution depends on the eBus SDK. The bootstrap and
30
31
  # schema-0 stay clean, so a flat-panel install never pulls it in — which is
31
32
  # what bounds the release coupling this dependency introduces to panels on
@@ -116,7 +116,7 @@ def _optional_integer(device: DiscoveredDevice, node: str, prop: str) -> int | N
116
116
  return None if raw is None else int(raw)
117
117
 
118
118
 
119
- def circuit_tabs(device: DiscoveredDevice) -> list[int]:
119
+ def _tabs(device: DiscoveredDevice) -> list[int]:
120
120
  """Breaker spaces from ``info/spaces``.
121
121
 
122
122
  v1.0 publishes the occupied spaces literally (``"36,38"``), where the flat
@@ -254,7 +254,7 @@ def build_circuit(
254
254
  # circuit's.
255
255
  produced_energy_wh=_number(device, NODE_METER, PROP_IMPORTED_ENERGY),
256
256
  consumed_energy_wh=_number(device, NODE_METER, PROP_EXPORTED_ENERGY),
257
- tabs=circuit_tabs(device),
257
+ tabs=_tabs(device),
258
258
  priority=priority,
259
259
  # `always-on` is `not relay-controllable`, and the flat schema derived
260
260
  # user-controllability from `always-on` — so this is the same answer by
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING
13
13
 
14
14
  from span_panel_api.models import ExtensionSubject, SpanPanelSnapshot, SpanPVSnapshot
15
15
  from span_panel_api_schema_1.adoption import build_adopted_devices
16
- from span_panel_api_schema_1.circuits import build_circuit, circuit_tabs
16
+ from span_panel_api_schema_1.circuits import build_circuit
17
17
  from span_panel_api_schema_1.const import (
18
18
  NODE_CONNECTION,
19
19
  NODE_INFO,
@@ -75,7 +75,8 @@ class TreeRoles:
75
75
  self.evse: list[DiscoveredDevice] = []
76
76
  self.bess: DiscoveredDevice | None = None
77
77
  # Every inverter, in tree order. From r202639 each commissioned inverter
78
- # is its own device; earlier firmware published one.
78
+ # is its own device; earlier firmware published one. None of them is
79
+ # primary.
79
80
  self.pvs: list[DiscoveredDevice] = []
80
81
  self.mid: DiscoveredDevice | None = None
81
82
 
@@ -94,33 +95,37 @@ class TreeRoles:
94
95
  elif declared == TYPE_MID and self.mid is None:
95
96
  self.mid = device
96
97
 
97
- self.pv: DiscoveredDevice | None = primary_pv(self.pvs, self.circuits)
98
98
 
99
+ def _pv_for(inverters: Sequence[SpanPVSnapshot]) -> SpanPVSnapshot:
100
+ """`SpanPanelSnapshot.pv`: the lone inverter, or the inverters together.
99
101
 
100
- def primary_pv(pvs: Sequence[DiscoveredDevice], circuits: Sequence[DiscoveredDevice]) -> DiscoveredDevice | None:
101
- """The inverter ``snapshot.pv`` describes, chosen the same way on every build.
102
-
103
- The order depends only on what the panel publishes, never on the order the
104
- devices were discovered in:
105
-
106
- 1. an inverter whose feeding circuit publishes its breaker spaces, before
107
- one with no feeding circuit (or a circuit that publishes none);
108
- 2. among those, the lowest breaker space the feeding circuit occupies;
109
- 3. then the lowest device id.
110
-
111
- A single inverter is chosen whatever it publishes, so a panel with one
112
- inverter reads exactly as it did before more than one could be published.
102
+ No inverter stands for the others. With several, `pv` identifies none of
103
+ them -- no device id, key, serial, firmware, feeding circuit or position --
104
+ and carries only what describes them together: the vendor and model where
105
+ every inverter shares one, the sum of their installed DC sizes where every
106
+ inverter publishes one, and their link as a three-valued AND (down if any
107
+ is reported down, up if every one is reported up, unknown otherwise).
108
+ Independent of order.
113
109
  """
114
- if not pvs:
115
- return None
116
- feeds = feed_circuit_ids(list(circuits))
117
- lowest_tab = {circuit.device_id: min(circuit_tabs(circuit), default=None) for circuit in circuits}
118
-
119
- def rank(device: DiscoveredDevice) -> tuple[int, int, str]:
120
- tab = lowest_tab.get(feeds.get(device.device_id, ""))
121
- return (0, tab, device.device_id) if tab is not None else (1, 0, device.device_id)
122
-
123
- return min(pvs, key=rank)
110
+ if not inverters:
111
+ return SpanPVSnapshot()
112
+ if len(inverters) == 1:
113
+ return inverters[0]
114
+ vendors = {inverter.vendor_name for inverter in inverters}
115
+ models = {inverter.model for inverter in inverters}
116
+ nameplates = [inverter.nameplate_capacity_w for inverter in inverters if inverter.nameplate_capacity_w is not None]
117
+ links = [inverter.connected for inverter in inverters]
118
+ connected: bool | None = None
119
+ if any(link is False for link in links):
120
+ connected = False
121
+ elif all(link is True for link in links):
122
+ connected = True
123
+ return SpanPVSnapshot(
124
+ vendor_name=vendors.pop() if len(vendors) == 1 else None,
125
+ model=models.pop() if len(models) == 1 else None,
126
+ nameplate_capacity_w=sum(nameplates) if len(nameplates) == len(inverters) else None,
127
+ connected=connected,
128
+ )
124
129
 
125
130
 
126
131
  def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], ready_since: float = 0.0) -> SpanPanelSnapshot:
@@ -138,10 +143,10 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
138
143
  # A DER's device type decides how its feeding circuit is labelled, so the
139
144
  # circuit inherits it — matching the flat adapter, where the same circuit
140
145
  # reports device_type "pv" rather than "circuit". Every inverter's circuit,
141
- # not only the one `snapshot.pv` describes: a PV circuit left labeled
142
- # "circuit" is read as a load, with its power in the opposite sign. Read
143
- # from the circuit side rather than through `feeds`, which keeps one circuit
144
- # per DER: every circuit of a DER fed by several carries its power.
146
+ # however many are commissioned: a PV circuit left labeled "circuit" is
147
+ # read as a load, with its power in the opposite sign. Read from the
148
+ # circuit side rather than through `feeds`, which keeps one circuit per
149
+ # DER: every circuit of a DER fed by several carries its power.
145
150
  kind_by_der = {device.device_id: "pv" for device in roles.pvs} | {device.device_id: "evse" for device in roles.evse}
146
151
  der_type_by_circuit = {
147
152
  circuit.device_id: kind
@@ -214,7 +219,7 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
214
219
  ("panel", panel),
215
220
  ("battery", roles.bess),
216
221
  ("mid", roles.mid),
217
- *((("pv", roles.pv),) if len(roles.pvs) == 1 else ()),
222
+ *((("pv", roles.pvs[0]),) if len(roles.pvs) == 1 else ()),
218
223
  )
219
224
  if device is not None
220
225
  ]
@@ -297,7 +302,7 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
297
302
  firmware_version=fields.firmware_version,
298
303
  power_flow_battery=fields.power_flow_battery,
299
304
  ),
300
- pv=pv_inverters[pv_inverter_key(roles.pv, feeds)] if roles.pv is not None else SpanPVSnapshot(),
305
+ pv=_pv_for(list(pv_inverters.values())),
301
306
  pv_inverters=pv_inverters,
302
307
  mid=build_mid(roles.mid, device_names),
303
308
  # Gated on the node being declared, not on any value: every limit this