pylxpweb 0.1.0__py3-none-any.whl → 0.5.2__py3-none-any.whl
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.
- pylxpweb/__init__.py +47 -2
- pylxpweb/api_namespace.py +241 -0
- pylxpweb/cli/__init__.py +3 -0
- pylxpweb/cli/collect_device_data.py +874 -0
- pylxpweb/client.py +387 -26
- pylxpweb/constants/__init__.py +481 -0
- pylxpweb/constants/api.py +48 -0
- pylxpweb/constants/devices.py +98 -0
- pylxpweb/constants/locations.py +227 -0
- pylxpweb/{constants.py → constants/registers.py} +72 -238
- pylxpweb/constants/scaling.py +479 -0
- pylxpweb/devices/__init__.py +32 -0
- pylxpweb/devices/_firmware_update_mixin.py +504 -0
- pylxpweb/devices/_mid_runtime_properties.py +1427 -0
- pylxpweb/devices/base.py +122 -0
- pylxpweb/devices/battery.py +589 -0
- pylxpweb/devices/battery_bank.py +331 -0
- pylxpweb/devices/inverters/__init__.py +32 -0
- pylxpweb/devices/inverters/_features.py +378 -0
- pylxpweb/devices/inverters/_runtime_properties.py +596 -0
- pylxpweb/devices/inverters/base.py +2124 -0
- pylxpweb/devices/inverters/generic.py +192 -0
- pylxpweb/devices/inverters/hybrid.py +274 -0
- pylxpweb/devices/mid_device.py +183 -0
- pylxpweb/devices/models.py +126 -0
- pylxpweb/devices/parallel_group.py +364 -0
- pylxpweb/devices/station.py +908 -0
- pylxpweb/endpoints/control.py +980 -2
- pylxpweb/endpoints/devices.py +249 -16
- pylxpweb/endpoints/firmware.py +43 -10
- pylxpweb/endpoints/plants.py +15 -19
- pylxpweb/exceptions.py +4 -0
- pylxpweb/models.py +708 -41
- pylxpweb/transports/__init__.py +78 -0
- pylxpweb/transports/capabilities.py +101 -0
- pylxpweb/transports/data.py +501 -0
- pylxpweb/transports/exceptions.py +59 -0
- pylxpweb/transports/factory.py +119 -0
- pylxpweb/transports/http.py +329 -0
- pylxpweb/transports/modbus.py +617 -0
- pylxpweb/transports/protocol.py +217 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/METADATA +130 -85
- pylxpweb-0.5.2.dist-info/RECORD +52 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.2.dist-info}/WHEEL +1 -1
- pylxpweb-0.5.2.dist-info/entry_points.txt +3 -0
- pylxpweb-0.1.0.dist-info/RECORD +0 -19
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Device and entity models for pylxpweb.
|
|
2
|
+
|
|
3
|
+
This module provides generic models for representing devices and their
|
|
4
|
+
entities (sensors, controls, etc.) in a platform-agnostic way.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeviceInfo(BaseModel):
|
|
15
|
+
"""Device information model.
|
|
16
|
+
|
|
17
|
+
Represents a physical device with metadata and hierarchy information.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
```python
|
|
21
|
+
device_info = DeviceInfo(
|
|
22
|
+
identifiers={("pylxpweb", "inverter_1234567890")},
|
|
23
|
+
name="FlexBOSS21 Inverter",
|
|
24
|
+
manufacturer="EG4 Electronics",
|
|
25
|
+
model="FlexBOSS21",
|
|
26
|
+
sw_version="34",
|
|
27
|
+
via_device=("pylxpweb", "station_12345"),
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
identifiers: set[tuple[str, str]] = Field(
|
|
33
|
+
description="Set of (domain, unique_id) tuples that identify this device"
|
|
34
|
+
)
|
|
35
|
+
name: str = Field(description="Human-readable device name")
|
|
36
|
+
manufacturer: str = Field(description="Device manufacturer")
|
|
37
|
+
model: str = Field(description="Device model name")
|
|
38
|
+
sw_version: str | None = Field(default=None, description="Software/firmware version")
|
|
39
|
+
via_device: tuple[str, str] | None = Field(
|
|
40
|
+
default=None, description="Parent device identifier (domain, unique_id)"
|
|
41
|
+
)
|
|
42
|
+
hw_version: str | None = Field(default=None, description="Hardware version")
|
|
43
|
+
configuration_url: str | None = Field(
|
|
44
|
+
default=None, description="URL to device configuration page"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
model_config = {"frozen": False}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Entity(BaseModel):
|
|
51
|
+
"""Entity representation model.
|
|
52
|
+
|
|
53
|
+
Represents a single data point or control (sensor, switch, button, etc.)
|
|
54
|
+
from a device. Devices can have multiple entities.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
```python
|
|
58
|
+
entity = Entity(
|
|
59
|
+
unique_id="inverter_1234567890_pac",
|
|
60
|
+
name="Inverter 1234567890 AC Power",
|
|
61
|
+
device_class="power",
|
|
62
|
+
state_class="measurement",
|
|
63
|
+
unit_of_measurement="W",
|
|
64
|
+
value=1030.5,
|
|
65
|
+
attributes={"voltage": 240.0, "frequency": 60.0},
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
unique_id: str = Field(description="Unique identifier for this entity")
|
|
71
|
+
name: str = Field(description="Human-readable entity name")
|
|
72
|
+
device_class: str | None = Field(
|
|
73
|
+
default=None, description="Device class (power, energy, temperature, etc.)"
|
|
74
|
+
)
|
|
75
|
+
state_class: str | None = Field(
|
|
76
|
+
default=None, description="State class (measurement, total, total_increasing)"
|
|
77
|
+
)
|
|
78
|
+
unit_of_measurement: str | None = Field(
|
|
79
|
+
default=None, description="Unit of measurement (W, kWh, %, °C, V, etc.)"
|
|
80
|
+
)
|
|
81
|
+
value: Any = Field(description="Current entity value/state")
|
|
82
|
+
attributes: dict[str, Any] = Field(
|
|
83
|
+
default_factory=dict, description="Additional entity attributes"
|
|
84
|
+
)
|
|
85
|
+
icon: str | None = Field(default=None, description="Icon identifier (e.g., mdi:solar-power)")
|
|
86
|
+
entity_category: str | None = Field(
|
|
87
|
+
default=None, description="Entity category (config, diagnostic)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
model_config = {"frozen": False}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Standard device classes (platform-agnostic)
|
|
94
|
+
class DeviceClass:
|
|
95
|
+
"""Standard device classes for entity categorization."""
|
|
96
|
+
|
|
97
|
+
# Sensor device classes
|
|
98
|
+
POWER = "power"
|
|
99
|
+
ENERGY = "energy"
|
|
100
|
+
BATTERY = "battery"
|
|
101
|
+
VOLTAGE = "voltage"
|
|
102
|
+
CURRENT = "current"
|
|
103
|
+
TEMPERATURE = "temperature"
|
|
104
|
+
FREQUENCY = "frequency"
|
|
105
|
+
POWER_FACTOR = "power_factor"
|
|
106
|
+
|
|
107
|
+
# Binary sensor device classes
|
|
108
|
+
CONNECTIVITY = "connectivity"
|
|
109
|
+
PROBLEM = "problem"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Standard state classes
|
|
113
|
+
class StateClass:
|
|
114
|
+
"""Standard state classes for entity state behavior."""
|
|
115
|
+
|
|
116
|
+
MEASUREMENT = "measurement"
|
|
117
|
+
TOTAL = "total"
|
|
118
|
+
TOTAL_INCREASING = "total_increasing"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Standard entity categories
|
|
122
|
+
class EntityCategory:
|
|
123
|
+
"""Standard entity categories for organization."""
|
|
124
|
+
|
|
125
|
+
CONFIG = "config"
|
|
126
|
+
DIAGNOSTIC = "diagnostic"
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""ParallelGroup class for inverters in parallel operation.
|
|
2
|
+
|
|
3
|
+
This module provides the ParallelGroup class that represents a group of
|
|
4
|
+
inverters operating in parallel, optionally with a MID (GridBOSS) device.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pylxpweb import LuxpowerClient
|
|
13
|
+
from pylxpweb.models import EnergyInfo
|
|
14
|
+
|
|
15
|
+
from .inverters.base import BaseInverter
|
|
16
|
+
from .mid_device import MIDDevice
|
|
17
|
+
from .station import Station
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParallelGroup:
|
|
21
|
+
"""Represents a group of inverters operating in parallel.
|
|
22
|
+
|
|
23
|
+
In the Luxpower/EG4 system, multiple inverters can operate in parallel
|
|
24
|
+
to increase total power capacity. The parallel group may include:
|
|
25
|
+
- Multiple inverters (2 or more)
|
|
26
|
+
- Optional MID device (GridBOSS) for grid management
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```python
|
|
30
|
+
# Access parallel groups from station
|
|
31
|
+
station = await client.get_station(plant_id)
|
|
32
|
+
|
|
33
|
+
for group in station.parallel_groups:
|
|
34
|
+
print(f"Group {group.name}: {len(group.inverters)} inverters")
|
|
35
|
+
|
|
36
|
+
if group.mid_device:
|
|
37
|
+
print(f" GridBOSS: {group.mid_device.serial_number}")
|
|
38
|
+
|
|
39
|
+
for inverter in group.inverters:
|
|
40
|
+
await inverter.refresh()
|
|
41
|
+
print(f" Inverter {inverter.serial_number}: {inverter.ac_output_power}W")
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
client: LuxpowerClient,
|
|
48
|
+
station: Station,
|
|
49
|
+
name: str,
|
|
50
|
+
first_device_serial: str,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize parallel group.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
client: LuxpowerClient instance for API access
|
|
56
|
+
station: Parent station object
|
|
57
|
+
name: Group identifier (typically "A", "B", etc.)
|
|
58
|
+
first_device_serial: Serial number of first device in group
|
|
59
|
+
"""
|
|
60
|
+
self._client = client
|
|
61
|
+
self.station = station
|
|
62
|
+
self.name = name
|
|
63
|
+
self.first_device_serial = first_device_serial
|
|
64
|
+
|
|
65
|
+
# Device collections (loaded by factory methods)
|
|
66
|
+
self.inverters: list[BaseInverter] = []
|
|
67
|
+
self.mid_device: MIDDevice | None = None
|
|
68
|
+
|
|
69
|
+
# Energy data (private - use properties for access)
|
|
70
|
+
self._energy: EnergyInfo | None = None
|
|
71
|
+
|
|
72
|
+
async def refresh(self) -> None:
|
|
73
|
+
"""Refresh runtime data for all devices in group.
|
|
74
|
+
|
|
75
|
+
This refreshes:
|
|
76
|
+
- All inverters in the group
|
|
77
|
+
- MID device if present
|
|
78
|
+
- Parallel group energy data
|
|
79
|
+
"""
|
|
80
|
+
import asyncio
|
|
81
|
+
|
|
82
|
+
tasks = []
|
|
83
|
+
|
|
84
|
+
# Refresh all inverters (all inverters have refresh method)
|
|
85
|
+
for inverter in self.inverters:
|
|
86
|
+
tasks.append(inverter.refresh())
|
|
87
|
+
|
|
88
|
+
# Refresh MID device (check for None, mid_device always has refresh method)
|
|
89
|
+
if self.mid_device:
|
|
90
|
+
tasks.append(self.mid_device.refresh())
|
|
91
|
+
|
|
92
|
+
# Fetch parallel group energy data if we have inverters
|
|
93
|
+
if self.inverters:
|
|
94
|
+
first_serial = self.inverters[0].serial_number
|
|
95
|
+
tasks.append(self._fetch_energy_data(first_serial))
|
|
96
|
+
|
|
97
|
+
# Execute concurrently
|
|
98
|
+
if tasks:
|
|
99
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
100
|
+
|
|
101
|
+
async def _fetch_energy_data(self, serial_number: str) -> None:
|
|
102
|
+
"""Fetch parallel group energy data.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
serial_number: Serial number of first inverter in group.
|
|
106
|
+
"""
|
|
107
|
+
import logging
|
|
108
|
+
|
|
109
|
+
from pylxpweb.exceptions import LuxpowerAPIError, LuxpowerConnectionError
|
|
110
|
+
|
|
111
|
+
_logger = logging.getLogger(__name__)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
self._energy = await self._client.api.devices.get_parallel_energy(serial_number)
|
|
115
|
+
_logger.debug(
|
|
116
|
+
"Parallel group %s energy data fetched: todayYielding=%s",
|
|
117
|
+
self.name,
|
|
118
|
+
self._energy.todayYielding if self._energy else None,
|
|
119
|
+
)
|
|
120
|
+
except (LuxpowerAPIError, LuxpowerConnectionError) as e:
|
|
121
|
+
# Keep existing cached data on error, but log the failure
|
|
122
|
+
_logger.warning(
|
|
123
|
+
"Failed to fetch parallel group %s energy data: %s",
|
|
124
|
+
self.name,
|
|
125
|
+
e,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def get_combined_energy(self) -> dict[str, float]:
|
|
129
|
+
"""Get combined energy statistics for all inverters in group.
|
|
130
|
+
|
|
131
|
+
Uses the parallel group energy endpoint which returns aggregate data
|
|
132
|
+
for the entire parallel group instead of summing individual inverters.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary with 'today_kwh' and 'lifetime_kwh' totals.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If no inverters in the group to query
|
|
139
|
+
"""
|
|
140
|
+
if not self.inverters:
|
|
141
|
+
return {
|
|
142
|
+
"today_kwh": 0.0,
|
|
143
|
+
"lifetime_kwh": 0.0,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Use first inverter serial to query parallel group energy
|
|
147
|
+
# The API returns aggregate data for the entire group
|
|
148
|
+
first_serial = self.inverters[0].serial_number
|
|
149
|
+
energy_info = await self._client.api.devices.get_parallel_energy(first_serial)
|
|
150
|
+
|
|
151
|
+
# Energy values are in units of 0.1 kWh, divide by 10 for kWh
|
|
152
|
+
return {
|
|
153
|
+
"today_kwh": energy_info.todayYielding / 10,
|
|
154
|
+
"lifetime_kwh": energy_info.totalYielding / 10,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ===========================================
|
|
158
|
+
# Energy Properties - Today
|
|
159
|
+
# ===========================================
|
|
160
|
+
# Daily energy values reset at midnight (API server time).
|
|
161
|
+
# The client automatically invalidates cache on hour boundaries
|
|
162
|
+
# to minimize stale data, but cannot control API reset timing.
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def today_yielding(self) -> float:
|
|
166
|
+
"""Get today's PV generation in kWh.
|
|
167
|
+
|
|
168
|
+
This value resets daily at midnight (API-controlled timing).
|
|
169
|
+
The client invalidates cache on hour boundaries, but values
|
|
170
|
+
shortly after midnight may reflect stale API data.
|
|
171
|
+
|
|
172
|
+
For Home Assistant: Use SensorStateClass.TOTAL_INCREASING
|
|
173
|
+
to let HA's statistics handle resets automatically.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Today's yielding (÷10 for kWh), or 0.0 if no data.
|
|
177
|
+
"""
|
|
178
|
+
if self._energy is None:
|
|
179
|
+
return 0.0
|
|
180
|
+
from pylxpweb.constants import scale_energy_value
|
|
181
|
+
|
|
182
|
+
return scale_energy_value("todayYielding", self._energy.todayYielding, to_kwh=True)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def today_charging(self) -> float:
|
|
186
|
+
"""Get today's battery charging energy in kWh.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Today's charging (÷10 for kWh), or 0.0 if no data.
|
|
190
|
+
"""
|
|
191
|
+
if self._energy is None:
|
|
192
|
+
return 0.0
|
|
193
|
+
from pylxpweb.constants import scale_energy_value
|
|
194
|
+
|
|
195
|
+
return scale_energy_value("todayCharging", self._energy.todayCharging, to_kwh=True)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def today_discharging(self) -> float:
|
|
199
|
+
"""Get today's battery discharging energy in kWh.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Today's discharging (÷10 for kWh), or 0.0 if no data.
|
|
203
|
+
"""
|
|
204
|
+
if self._energy is None:
|
|
205
|
+
return 0.0
|
|
206
|
+
from pylxpweb.constants import scale_energy_value
|
|
207
|
+
|
|
208
|
+
return scale_energy_value("todayDischarging", self._energy.todayDischarging, to_kwh=True)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def today_import(self) -> float:
|
|
212
|
+
"""Get today's grid import energy in kWh.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Today's import (÷10 for kWh), or 0.0 if no data.
|
|
216
|
+
"""
|
|
217
|
+
if self._energy is None:
|
|
218
|
+
return 0.0
|
|
219
|
+
from pylxpweb.constants import scale_energy_value
|
|
220
|
+
|
|
221
|
+
return scale_energy_value("todayImport", self._energy.todayImport, to_kwh=True)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def today_export(self) -> float:
|
|
225
|
+
"""Get today's grid export energy in kWh.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Today's export (÷10 for kWh), or 0.0 if no data.
|
|
229
|
+
"""
|
|
230
|
+
if self._energy is None:
|
|
231
|
+
return 0.0
|
|
232
|
+
from pylxpweb.constants import scale_energy_value
|
|
233
|
+
|
|
234
|
+
return scale_energy_value("todayExport", self._energy.todayExport, to_kwh=True)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def today_usage(self) -> float:
|
|
238
|
+
"""Get today's energy usage in kWh.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Today's usage (÷10 for kWh), or 0.0 if no data.
|
|
242
|
+
"""
|
|
243
|
+
if self._energy is None:
|
|
244
|
+
return 0.0
|
|
245
|
+
from pylxpweb.constants import scale_energy_value
|
|
246
|
+
|
|
247
|
+
return scale_energy_value("todayUsage", self._energy.todayUsage, to_kwh=True)
|
|
248
|
+
|
|
249
|
+
# ===========================================
|
|
250
|
+
# Energy Properties - Total (Lifetime)
|
|
251
|
+
# ===========================================
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def total_yielding(self) -> float:
|
|
255
|
+
"""Get total lifetime PV generation in kWh.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Total yielding (÷10 for kWh), or 0.0 if no data.
|
|
259
|
+
"""
|
|
260
|
+
if self._energy is None:
|
|
261
|
+
return 0.0
|
|
262
|
+
from pylxpweb.constants import scale_energy_value
|
|
263
|
+
|
|
264
|
+
return scale_energy_value("totalYielding", self._energy.totalYielding, to_kwh=True)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def total_charging(self) -> float:
|
|
268
|
+
"""Get total lifetime battery charging energy in kWh.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Total charging (÷10 for kWh), or 0.0 if no data.
|
|
272
|
+
"""
|
|
273
|
+
if self._energy is None:
|
|
274
|
+
return 0.0
|
|
275
|
+
from pylxpweb.constants import scale_energy_value
|
|
276
|
+
|
|
277
|
+
return scale_energy_value("totalCharging", self._energy.totalCharging, to_kwh=True)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def total_discharging(self) -> float:
|
|
281
|
+
"""Get total lifetime battery discharging energy in kWh.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Total discharging (÷10 for kWh), or 0.0 if no data.
|
|
285
|
+
"""
|
|
286
|
+
if self._energy is None:
|
|
287
|
+
return 0.0
|
|
288
|
+
from pylxpweb.constants import scale_energy_value
|
|
289
|
+
|
|
290
|
+
return scale_energy_value("totalDischarging", self._energy.totalDischarging, to_kwh=True)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def total_import(self) -> float:
|
|
294
|
+
"""Get total lifetime grid import energy in kWh.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Total import (÷10 for kWh), or 0.0 if no data.
|
|
298
|
+
"""
|
|
299
|
+
if self._energy is None:
|
|
300
|
+
return 0.0
|
|
301
|
+
from pylxpweb.constants import scale_energy_value
|
|
302
|
+
|
|
303
|
+
return scale_energy_value("totalImport", self._energy.totalImport, to_kwh=True)
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def total_export(self) -> float:
|
|
307
|
+
"""Get total lifetime grid export energy in kWh.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Total export (÷10 for kWh), or 0.0 if no data.
|
|
311
|
+
"""
|
|
312
|
+
if self._energy is None:
|
|
313
|
+
return 0.0
|
|
314
|
+
from pylxpweb.constants import scale_energy_value
|
|
315
|
+
|
|
316
|
+
return scale_energy_value("totalExport", self._energy.totalExport, to_kwh=True)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def total_usage(self) -> float:
|
|
320
|
+
"""Get total lifetime energy usage in kWh.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Total usage (÷10 for kWh), or 0.0 if no data.
|
|
324
|
+
"""
|
|
325
|
+
if self._energy is None:
|
|
326
|
+
return 0.0
|
|
327
|
+
from pylxpweb.constants import scale_energy_value
|
|
328
|
+
|
|
329
|
+
return scale_energy_value("totalUsage", self._energy.totalUsage, to_kwh=True)
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
async def from_api_data(
|
|
333
|
+
cls,
|
|
334
|
+
client: LuxpowerClient,
|
|
335
|
+
station: Station,
|
|
336
|
+
group_data: dict[str, Any],
|
|
337
|
+
) -> ParallelGroup:
|
|
338
|
+
"""Factory method to create ParallelGroup from API data.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
client: LuxpowerClient instance
|
|
342
|
+
station: Parent station object
|
|
343
|
+
group_data: API response data for parallel group
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
ParallelGroup instance with devices loaded.
|
|
347
|
+
"""
|
|
348
|
+
# Extract group info
|
|
349
|
+
name = group_data.get("parallelGroup", "A")
|
|
350
|
+
first_serial = group_data.get("parallelFirstDeviceSn", "")
|
|
351
|
+
|
|
352
|
+
# Create group
|
|
353
|
+
group = cls(
|
|
354
|
+
client=client,
|
|
355
|
+
station=station,
|
|
356
|
+
name=name,
|
|
357
|
+
first_device_serial=first_serial,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Note: Inverters and MID device will be loaded by Station._load_devices()
|
|
361
|
+
# This is because device creation requires model-specific inverter classes
|
|
362
|
+
# which will be implemented in Phase 2
|
|
363
|
+
|
|
364
|
+
return group
|