span-panel-api-schema-1 1.1.3__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.
Files changed (39) hide show
  1. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/CHANGELOG.md +12 -0
  2. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/PKG-INFO +1 -1
  3. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/pyproject.toml +1 -1
  4. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/adapter.py +22 -6
  5. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/field_metadata.py +2 -2
  6. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/panel.py +14 -12
  7. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/.gitignore +0 -0
  8. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/README.md +0 -0
  9. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/breaker.json +0 -0
  10. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/charge-limit.json +0 -0
  11. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/connection.json +0 -0
  12. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/door.json +0 -0
  13. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/grid-forming.json +0 -0
  14. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/grid.json +0 -0
  15. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/info.json +0 -0
  16. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/load-shed.json +0 -0
  17. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/meter.json +0 -0
  18. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/pcs.json +0 -0
  19. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/power-flows.json +0 -0
  20. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/shed-forecast.json +0 -0
  21. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/shed.json +0 -0
  22. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/soc.json +0 -0
  23. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/status.json +0 -0
  24. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/catalogs/switch.json +0 -0
  25. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/spec/registries/device-types.md +0 -0
  26. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/__init__.py +0 -0
  27. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/adoption.py +0 -0
  28. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/catalog.py +0 -0
  29. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/charge_limit.py +0 -0
  30. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/circuits.py +0 -0
  31. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/const.py +0 -0
  32. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/description.py +0 -0
  33. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/devices.py +0 -0
  34. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/extension.py +0 -0
  35. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/py.typed +0 -0
  36. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/reference/parent_child_tree.json +0 -0
  37. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/snapshot.py +0 -0
  38. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/spec_lock.json +0 -0
  39. {span_panel_api_schema_1-1.1.3 → span_panel_api_schema_1-1.1.4}/src/span_panel_api_schema_1/transport.py +0 -0
@@ -9,6 +9,18 @@ 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
+
12
24
  ## [1.1.3]
13
25
 
14
26
  `spec_lock.json` ships inside this wheel, so its shape changing 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
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api-schema-1"
3
- version = "1.1.3"
3
+ version = "1.1.4"
4
4
  description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -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, off-grid, or no assertion. Anything not recognised
295
- returns None rather than guessing, so the transport refuses the command
296
- instead of asserting something the user did not ask for.
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 2 of resolve_islanding_state (panel.py), shadowed wherever a MID answers "
476
- "at tier 1, and the write target of set_dominant_power_source_target"
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 "
@@ -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 | MID `$state` is `ready` and `islanding-state` present | sensed |
612
- | 2 | MID not `ready` | `shed/asserted-islanding-state`, when not `NONE` |
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
- **Tier 2 is the reason the assertion control exists.** When comms to the BESS or MID
617
- are lost and the grid returns, the user asserts the grid is up so the BESS stops
618
- discharging. Declining to read it here would wire the control and then ignore it at
619
- exactly the moment it matters. Nothing is hidden by doing so: the MID is a device, so
620
- it goes *unavailable* in Home Assistant when it stops publishing, and the assertion is
621
- itself visible as the control the user set.
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")