span-panel-api-schema-1 1.2.0__tar.gz → 1.2.1b1__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.1b1}/CHANGELOG.md +13 -1
  2. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/PKG-INFO +2 -2
  3. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/pyproject.toml +3 -2
  4. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/snapshot.py +37 -32
  5. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/.gitignore +0 -0
  6. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/README.md +0 -0
  7. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/breaker.json +0 -0
  8. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/charge-limit.json +0 -0
  9. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/connection.json +0 -0
  10. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/door.json +0 -0
  11. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/grid-forming.json +0 -0
  12. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/grid.json +0 -0
  13. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/info.json +0 -0
  14. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/load-shed.json +0 -0
  15. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/meter.json +0 -0
  16. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/pcs.json +0 -0
  17. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/power-flows.json +0 -0
  18. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/shed-forecast.json +0 -0
  19. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/shed.json +0 -0
  20. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/soc.json +0 -0
  21. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/status.json +0 -0
  22. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/catalogs/switch.json +0 -0
  23. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/spec/registries/device-types.md +0 -0
  24. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/__init__.py +0 -0
  25. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/adapter.py +0 -0
  26. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/adoption.py +0 -0
  27. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/catalog.py +0 -0
  28. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/charge_limit.py +0 -0
  29. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/src/span_panel_api_schema_1/circuits.py +0 -0
  30. {span_panel_api_schema_1-1.2.0 → span_panel_api_schema_1-1.2.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/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.1b1}/src/span_panel_api_schema_1/transport.py +0 -0
@@ -9,7 +9,19 @@ 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.1b1]
13
+
14
+ No inverter is primary any more. Requires `span-panel-api` **3.6.1 or newer**. 1.2.0 was withdrawn from PyPI; this release carries its changes, listed under 1.2.0 below, except how `pv` is chosen, which this release replaces.
15
+
16
+ ### Changed
17
+
18
+ - **`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 the one on the lowest breaker space.
19
+
20
+ ### Removed
21
+
22
+ - **`snapshot.primary_pv` and `TreeRoles.pv`**, which singled out one inverter as `pv`.
23
+
24
+ ## [1.2.0] [YANKED]
13
25
 
14
26
  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**.
15
27
 
@@ -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.1b1
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.1b1
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.1b1"
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.1b1, whose SpanPanelSnapshot.pv documents what this parser builds.
29
+ "span-panel-api>=3.6.1b1,<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
@@ -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