span-panel-api 2.2.0__tar.gz → 2.2.2__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 (22) hide show
  1. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/PKG-INFO +1 -1
  2. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/pyproject.toml +1 -1
  3. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/__init__.py +9 -2
  4. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/client.py +19 -4
  5. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/homie.py +7 -7
  6. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/protocol.py +7 -0
  7. span_panel_api-2.2.2/src/span_panel_api/py.typed +0 -0
  8. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/simulation.py +12 -1
  9. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/LICENSE +0 -0
  10. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/README.md +0 -0
  11. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/auth.py +0 -0
  12. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/const.py +0 -0
  13. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/detection.py +0 -0
  14. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/exceptions.py +0 -0
  15. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/factory.py +0 -0
  16. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/models.py +0 -0
  17. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/__init__.py +0 -0
  18. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/async_client.py +0 -0
  19. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/connection.py +0 -0
  20. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/const.py +0 -0
  21. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/models.py +0 -0
  22. {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/phase_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: span-panel-api
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: A client library for SPAN Panel API
5
5
  License-File: LICENSE
6
6
  Author: SpanPanel
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api"
3
- version = "2.2.0"
3
+ version = "2.2.2"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -35,15 +35,22 @@ from .phase_validation import (
35
35
  suggest_balanced_pairing,
36
36
  validate_solar_tabs,
37
37
  )
38
- from .protocol import CircuitControlProtocol, PanelCapability, SpanPanelClientProtocol, StreamingCapableProtocol
38
+ from .protocol import (
39
+ CircuitControlProtocol,
40
+ PanelCapability,
41
+ PanelControlProtocol,
42
+ SpanPanelClientProtocol,
43
+ StreamingCapableProtocol,
44
+ )
39
45
  from .simulation import DynamicSimulationEngine
40
46
 
41
- __version__ = "2.0.0"
47
+ __version__ = "2.2.1"
42
48
  # fmt: off
43
49
  __all__ = [ # noqa: RUF022
44
50
  # Protocols
45
51
  "CircuitControlProtocol",
46
52
  "PanelCapability",
53
+ "PanelControlProtocol",
47
54
  "SpanPanelClientProtocol",
48
55
  "StreamingCapableProtocol",
49
56
  # Snapshots
@@ -1,8 +1,8 @@
1
1
  """SPAN Panel MQTT client.
2
2
 
3
3
  Composes AsyncMqttBridge and HomieDeviceConsumer to implement
4
- SpanPanelClientProtocol, CircuitControlProtocol, and
5
- StreamingCapableProtocol.
4
+ SpanPanelClientProtocol, CircuitControlProtocol,
5
+ PanelControlProtocol, and StreamingCapableProtocol.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -11,11 +11,11 @@ import asyncio
11
11
  from collections.abc import Awaitable, Callable
12
12
  import logging
13
13
 
14
- from ..exceptions import SpanPanelConnectionError
14
+ from ..exceptions import SpanPanelConnectionError, SpanPanelServerError
15
15
  from ..models import SpanPanelSnapshot
16
16
  from ..protocol import PanelCapability
17
17
  from .connection import AsyncMqttBridge
18
- from .const import MQTT_READY_TIMEOUT_S, PROPERTY_SET_TOPIC_FMT, WILDCARD_TOPIC_FMT
18
+ from .const import MQTT_READY_TIMEOUT_S, PROPERTY_SET_TOPIC_FMT, TYPE_CORE, WILDCARD_TOPIC_FMT
19
19
  from .homie import HomieDeviceConsumer
20
20
  from .models import MqttClientConfig
21
21
 
@@ -180,6 +180,21 @@ class SpanMqttClient:
180
180
  if self._bridge is not None:
181
181
  self._bridge.publish(topic, priority, qos=1)
182
182
 
183
+ # -- PanelControlProtocol ----------------------------------------------
184
+
185
+ async def set_dominant_power_source(self, value: str) -> None:
186
+ """Publish dominant-power-source change to the core node.
187
+
188
+ Args:
189
+ value: DPS enum value (GRID, BATTERY, NONE, GENERATOR, PV)
190
+ """
191
+ core_node = self._homie.find_node_by_type(TYPE_CORE)
192
+ if core_node is None:
193
+ raise SpanPanelServerError("Core node not found in panel topology")
194
+ topic = PROPERTY_SET_TOPIC_FMT.format(serial=self._serial_number, node=core_node, prop="dominant-power-source")
195
+ if self._bridge is not None:
196
+ self._bridge.publish(topic, value, qos=1)
197
+
183
198
  # -- StreamingCapableProtocol ------------------------------------------
184
199
 
185
200
  def register_snapshot_callback(
@@ -188,7 +188,7 @@ class HomieDeviceConsumer:
188
188
  """Get a property timestamp."""
189
189
  return self._property_timestamps.get(node_id, {}).get(prop_id, 0)
190
190
 
191
- def _find_node_by_type(self, type_string: str) -> str | None:
191
+ def find_node_by_type(self, type_string: str) -> str | None:
192
192
  """Find the first node ID matching a given type."""
193
193
  for node_id, node_type in self._node_types.items():
194
194
  if node_type == type_string:
@@ -319,7 +319,7 @@ class HomieDeviceConsumer:
319
319
 
320
320
  def _build_battery(self) -> SpanBatterySnapshot:
321
321
  """Build battery snapshot from BESS node."""
322
- bess_node = self._find_node_by_type(TYPE_BESS)
322
+ bess_node = self.find_node_by_type(TYPE_BESS)
323
323
  if bess_node is None:
324
324
  return SpanBatterySnapshot()
325
325
 
@@ -348,7 +348,7 @@ class HomieDeviceConsumer:
348
348
 
349
349
  def _build_pv(self) -> SpanPVSnapshot:
350
350
  """Build PV snapshot from the first PV metadata node."""
351
- pv_node = self._find_node_by_type(TYPE_PV)
351
+ pv_node = self.find_node_by_type(TYPE_PV)
352
352
  if pv_node is None:
353
353
  return SpanPVSnapshot()
354
354
 
@@ -396,7 +396,7 @@ class HomieDeviceConsumer:
396
396
  4. both grid signals zero AND DPS != GRID — islanded
397
397
  """
398
398
  # 1. BESS grid-state is authoritative when available
399
- bess_node = self._find_node_by_type(TYPE_BESS)
399
+ bess_node = self.find_node_by_type(TYPE_BESS)
400
400
  if bess_node is not None:
401
401
  gs = self._get_prop(bess_node, "grid-state")
402
402
  if gs == "ON_GRID":
@@ -482,7 +482,7 @@ class HomieDeviceConsumer:
482
482
 
483
483
  def _build_snapshot(self) -> SpanPanelSnapshot:
484
484
  """Build full snapshot from accumulated property values."""
485
- core_node = self._find_node_by_type(TYPE_CORE)
485
+ core_node = self.find_node_by_type(TYPE_CORE)
486
486
  upstream_lugs = self._find_lugs_node(LUGS_UPSTREAM)
487
487
  downstream_lugs = self._find_lugs_node(LUGS_DOWNSTREAM)
488
488
 
@@ -569,7 +569,7 @@ class HomieDeviceConsumer:
569
569
  downstream_l2_current = _parse_float(dl2_i) if dl2_i else None
570
570
 
571
571
  # Power flows
572
- pf_node = self._find_node_by_type(TYPE_POWER_FLOWS)
572
+ pf_node = self.find_node_by_type(TYPE_POWER_FLOWS)
573
573
  power_flow_pv: float | None = None
574
574
  power_flow_battery: float | None = None
575
575
  power_flow_grid: float | None = None
@@ -607,7 +607,7 @@ class HomieDeviceConsumer:
607
607
  evse = self._build_evse_devices()
608
608
 
609
609
  # BESS grid state for v2-native field
610
- bess_node = self._find_node_by_type(TYPE_BESS)
610
+ bess_node = self.find_node_by_type(TYPE_BESS)
611
611
  grid_state: str | None = None
612
612
  if bess_node is not None:
613
613
  gs = self._get_prop(bess_node, "grid-state")
@@ -53,6 +53,13 @@ class CircuitControlProtocol(Protocol):
53
53
  async def set_circuit_priority(self, circuit_id: str, priority: str) -> None: ...
54
54
 
55
55
 
56
+ @runtime_checkable
57
+ class PanelControlProtocol(Protocol):
58
+ """Control protocol for panel-level settable properties."""
59
+
60
+ async def set_dominant_power_source(self, value: str) -> None: ...
61
+
62
+
56
63
  @runtime_checkable
57
64
  class StreamingCapableProtocol(Protocol):
58
65
  """Push-based transport that delivers updates via callbacks."""
File without changes
@@ -1168,7 +1168,18 @@ class DynamicSimulationEngine:
1168
1168
 
1169
1169
  # --- Battery ---
1170
1170
  soe_percentage = float(soe_data["soe"]["percentage"])
1171
- battery_snapshot = SpanBatterySnapshot(soe_percentage=soe_percentage)
1171
+ nameplate_kwh = 13.5 # Simulated battery capacity
1172
+ soe_kwh = nameplate_kwh * soe_percentage / 100.0
1173
+ battery_snapshot = SpanBatterySnapshot(
1174
+ soe_percentage=soe_percentage,
1175
+ soe_kwh=soe_kwh,
1176
+ vendor_name="Simulated BESS",
1177
+ product_name="Battery Storage",
1178
+ serial_number=f"SIM-BESS-{raw['status']['system']['serial']}",
1179
+ software_version="1.0.0-sim",
1180
+ nameplate_capacity_kwh=nameplate_kwh,
1181
+ connected=True,
1182
+ )
1172
1183
 
1173
1184
  # --- EVSE ---
1174
1185
  evse_devices: dict[str, SpanEvseSnapshot] = {}
File without changes
File without changes