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.
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/PKG-INFO +1 -1
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/pyproject.toml +1 -1
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/__init__.py +9 -2
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/client.py +19 -4
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/homie.py +7 -7
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/protocol.py +7 -0
- span_panel_api-2.2.2/src/span_panel_api/py.typed +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/simulation.py +12 -1
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/LICENSE +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/README.md +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/const.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/models.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-2.2.0 → span_panel_api-2.2.2}/src/span_panel_api/phase_validation.py +0 -0
|
@@ -35,15 +35,22 @@ from .phase_validation import (
|
|
|
35
35
|
suggest_balanced_pairing,
|
|
36
36
|
validate_solar_tabs,
|
|
37
37
|
)
|
|
38
|
-
from .protocol import
|
|
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.
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|