pylxpweb 0.1.0__py3-none-any.whl → 0.5.0__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 +545 -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 +351 -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 +629 -40
- pylxpweb/transports/__init__.py +78 -0
- pylxpweb/transports/capabilities.py +101 -0
- pylxpweb/transports/data.py +495 -0
- pylxpweb/transports/exceptions.py +59 -0
- pylxpweb/transports/factory.py +119 -0
- pylxpweb/transports/http.py +329 -0
- pylxpweb/transports/modbus.py +557 -0
- pylxpweb/transports/protocol.py +217 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/METADATA +130 -85
- pylxpweb-0.5.0.dist-info/RECORD +52 -0
- {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/WHEEL +1 -1
- pylxpweb-0.5.0.dist-info/entry_points.txt +3 -0
- pylxpweb-0.1.0.dist-info/RECORD +0 -19
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Battery bank module for aggregate battery monitoring.
|
|
2
|
+
|
|
3
|
+
This module provides the BatteryBank class that represents the aggregate
|
|
4
|
+
battery system data (total capacity, charge/discharge power, overall status)
|
|
5
|
+
for all batteries connected to an inverter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from pylxpweb.constants import ScaleFactor, apply_scale
|
|
13
|
+
|
|
14
|
+
from .base import BaseDevice
|
|
15
|
+
from .models import DeviceInfo, Entity
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pylxpweb import LuxpowerClient
|
|
19
|
+
from pylxpweb.models import BatteryInfo
|
|
20
|
+
|
|
21
|
+
from .battery import Battery
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BatteryBank(BaseDevice):
|
|
25
|
+
"""Represents the aggregate battery bank for an inverter.
|
|
26
|
+
|
|
27
|
+
This class provides aggregate information for all batteries connected
|
|
28
|
+
to an inverter, including total capacity, charge/discharge power, and
|
|
29
|
+
overall status.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
```python
|
|
33
|
+
# BatteryBank is typically created from BatteryInfo API response
|
|
34
|
+
battery_info = await client.api.devices.get_battery_info(serial_num)
|
|
35
|
+
|
|
36
|
+
battery_bank = BatteryBank(
|
|
37
|
+
client=client,
|
|
38
|
+
inverter_serial=serial_num,
|
|
39
|
+
battery_info=battery_info
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
print(f"Battery Bank Status: {battery_bank.status}")
|
|
43
|
+
print(f"Total Capacity: {battery_bank.max_capacity} Ah")
|
|
44
|
+
print(f"Current Capacity: {battery_bank.current_capacity} Ah")
|
|
45
|
+
print(f"SOC: {battery_bank.soc}%")
|
|
46
|
+
print(f"Charge Power: {battery_bank.charge_power}W")
|
|
47
|
+
```
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
client: LuxpowerClient,
|
|
53
|
+
inverter_serial: str,
|
|
54
|
+
battery_info: BatteryInfo,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize battery bank.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
client: LuxpowerClient instance for API access
|
|
60
|
+
inverter_serial: Serial number of parent inverter
|
|
61
|
+
battery_info: BatteryInfo data from API
|
|
62
|
+
"""
|
|
63
|
+
# Use inverter serial + "_battery_bank" as unique ID
|
|
64
|
+
super().__init__(client, f"{inverter_serial}_battery_bank", "Battery Bank")
|
|
65
|
+
|
|
66
|
+
self.inverter_serial = inverter_serial
|
|
67
|
+
self.data = battery_info
|
|
68
|
+
|
|
69
|
+
# Individual battery modules in this bank
|
|
70
|
+
self.batteries: list[Battery] = [] # Will be Battery objects
|
|
71
|
+
|
|
72
|
+
# ========== Status Properties ==========
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def status(self) -> str:
|
|
76
|
+
"""Get battery bank charging status.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Status string (e.g., "Charging", "Discharging", "Idle").
|
|
80
|
+
"""
|
|
81
|
+
return self.data.batStatus
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def status_text(self) -> str | None:
|
|
85
|
+
"""Get detailed status text.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Detailed status text, or None if not available.
|
|
89
|
+
"""
|
|
90
|
+
return self.data.statusText
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def is_lost(self) -> bool:
|
|
94
|
+
"""Check if battery communication is lost.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if battery is not communicating, False otherwise.
|
|
98
|
+
"""
|
|
99
|
+
return self.data.lost if self.data.lost is not None else False
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def has_runtime_data(self) -> bool:
|
|
103
|
+
"""Check if runtime data is available.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if runtime data is available, False otherwise.
|
|
107
|
+
"""
|
|
108
|
+
return self.data.hasRuntimeData if self.data.hasRuntimeData is not None else False
|
|
109
|
+
|
|
110
|
+
# ========== State of Charge ==========
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def soc(self) -> int:
|
|
114
|
+
"""Get aggregate state of charge for battery bank.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
State of charge percentage (0-100).
|
|
118
|
+
"""
|
|
119
|
+
return self.data.soc
|
|
120
|
+
|
|
121
|
+
# ========== Voltage Properties ==========
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def voltage(self) -> float:
|
|
125
|
+
"""Get battery bank voltage in volts.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Battery voltage (scaled from vBat ÷10).
|
|
129
|
+
"""
|
|
130
|
+
return apply_scale(self.data.vBat, ScaleFactor.SCALE_10)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def voltage_text(self) -> str | None:
|
|
134
|
+
"""Get formatted voltage text.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Voltage text (e.g., "53.8V"), or None if not available.
|
|
138
|
+
"""
|
|
139
|
+
return self.data.totalVoltageText
|
|
140
|
+
|
|
141
|
+
# ========== Power Properties ==========
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def charge_power(self) -> int:
|
|
145
|
+
"""Get total charging power in watts.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Charging power in watts.
|
|
149
|
+
"""
|
|
150
|
+
return self.data.pCharge
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def discharge_power(self) -> int:
|
|
154
|
+
"""Get total discharging power in watts.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Discharging power in watts.
|
|
158
|
+
"""
|
|
159
|
+
return self.data.pDisCharge
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def battery_power(self) -> int | None:
|
|
163
|
+
"""Get net battery power in watts (positive = charging, negative = discharging).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Net battery power in watts, or None if not available.
|
|
167
|
+
"""
|
|
168
|
+
return self.data.batPower
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def pv_power(self) -> int | None:
|
|
172
|
+
"""Get PV solar power in watts.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
PV power in watts, or None if not available.
|
|
176
|
+
"""
|
|
177
|
+
return self.data.ppv
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def inverter_power(self) -> int | None:
|
|
181
|
+
"""Get inverter power in watts.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Inverter power in watts, or None if not available.
|
|
185
|
+
"""
|
|
186
|
+
return self.data.pinv
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def grid_power(self) -> int | None:
|
|
190
|
+
"""Get grid power in watts.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Grid power in watts, or None if not available.
|
|
194
|
+
"""
|
|
195
|
+
return self.data.prec
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def eps_power(self) -> int | None:
|
|
199
|
+
"""Get EPS/backup power in watts.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
EPS power in watts, or None if not available.
|
|
203
|
+
"""
|
|
204
|
+
return self.data.peps
|
|
205
|
+
|
|
206
|
+
# ========== Capacity Properties ==========
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def max_capacity(self) -> int:
|
|
210
|
+
"""Get maximum battery bank capacity in amp-hours.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Maximum capacity in Ah.
|
|
214
|
+
"""
|
|
215
|
+
return self.data.maxBatteryCharge
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def current_capacity(self) -> float:
|
|
219
|
+
"""Get current battery bank capacity in amp-hours.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Current capacity in Ah, rounded to 1 decimal place.
|
|
223
|
+
"""
|
|
224
|
+
return round(self.data.currentBatteryCharge, 1)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def remain_capacity(self) -> int | None:
|
|
228
|
+
"""Get remaining capacity in amp-hours.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Remaining capacity in Ah, or None if not available.
|
|
232
|
+
"""
|
|
233
|
+
return self.data.remainCapacity
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def full_capacity(self) -> int | None:
|
|
237
|
+
"""Get full capacity in amp-hours.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Full capacity in Ah, or None if not available.
|
|
241
|
+
"""
|
|
242
|
+
return self.data.fullCapacity
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def capacity_percent(self) -> int | None:
|
|
246
|
+
"""Get capacity percentage.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Capacity percentage (0-100), or None if not available.
|
|
250
|
+
"""
|
|
251
|
+
return self.data.capacityPercent
|
|
252
|
+
|
|
253
|
+
# ========== Current Properties ==========
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def current_text(self) -> str | None:
|
|
257
|
+
"""Get formatted current text.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Current text (e.g., "49.8A"), or None if not available.
|
|
261
|
+
"""
|
|
262
|
+
return self.data.currentText
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def current_type(self) -> str | None:
|
|
266
|
+
"""Get current flow direction.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
"charge" or "discharge", or None if not available.
|
|
270
|
+
"""
|
|
271
|
+
return self.data.currentType
|
|
272
|
+
|
|
273
|
+
# ========== Battery Count ==========
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def battery_count(self) -> int:
|
|
277
|
+
"""Get number of batteries in the bank.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Number of battery modules.
|
|
281
|
+
"""
|
|
282
|
+
if self.data.totalNumber is not None:
|
|
283
|
+
return self.data.totalNumber
|
|
284
|
+
return len(self.data.batteryArray)
|
|
285
|
+
|
|
286
|
+
async def refresh(self) -> None:
|
|
287
|
+
"""Refresh battery bank data.
|
|
288
|
+
|
|
289
|
+
Note: Battery bank data is refreshed through the parent inverter.
|
|
290
|
+
This method is a no-op for battery banks.
|
|
291
|
+
"""
|
|
292
|
+
# Battery bank data comes from inverter's getBatteryInfo call
|
|
293
|
+
# Individual battery banks don't have their own refresh endpoint
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
def to_device_info(self) -> DeviceInfo:
|
|
297
|
+
"""Convert to device info model.
|
|
298
|
+
|
|
299
|
+
Note: BatteryBank entities are not currently exposed to Home Assistant.
|
|
300
|
+
Aggregate battery data is available through inverter sensors.
|
|
301
|
+
This method is preserved for potential future use.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
DeviceInfo with battery bank metadata.
|
|
305
|
+
"""
|
|
306
|
+
return DeviceInfo(
|
|
307
|
+
identifiers={("pylxpweb", f"battery_bank_{self.inverter_serial}")},
|
|
308
|
+
name=f"Battery Bank ({self.inverter_serial})",
|
|
309
|
+
manufacturer="EG4/Luxpower",
|
|
310
|
+
model=f"Battery Bank ({self.battery_count} modules)",
|
|
311
|
+
via_device=("pylxpweb", f"inverter_{self.inverter_serial}"),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def to_entities(self) -> list[Entity]:
|
|
315
|
+
"""Generate entities for this battery bank.
|
|
316
|
+
|
|
317
|
+
Note: BatteryBank entities are not currently generated for Home Assistant
|
|
318
|
+
to avoid excessive entity proliferation. Aggregate battery data is available
|
|
319
|
+
through inverter sensors, and individual battery data is available through
|
|
320
|
+
Battery entities.
|
|
321
|
+
|
|
322
|
+
This method is preserved for potential future use if aggregate battery
|
|
323
|
+
entities are needed.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Empty list (entities not currently generated).
|
|
327
|
+
"""
|
|
328
|
+
# Return empty list - BatteryBank entities not needed for HA integration
|
|
329
|
+
# Aggregate data is accessible via inverter sensors
|
|
330
|
+
# Individual battery data is accessible via Battery entities
|
|
331
|
+
return []
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Inverter implementations for different EG4/Luxpower models."""
|
|
2
|
+
|
|
3
|
+
from ._features import (
|
|
4
|
+
DEVICE_TYPE_CODE_TO_FAMILY,
|
|
5
|
+
FAMILY_DEFAULT_FEATURES,
|
|
6
|
+
GridType,
|
|
7
|
+
InverterFamily,
|
|
8
|
+
InverterFeatures,
|
|
9
|
+
InverterModelInfo,
|
|
10
|
+
get_family_features,
|
|
11
|
+
get_inverter_family,
|
|
12
|
+
)
|
|
13
|
+
from .base import BaseInverter
|
|
14
|
+
from .generic import GenericInverter
|
|
15
|
+
from .hybrid import HybridInverter
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Inverter classes
|
|
19
|
+
"BaseInverter",
|
|
20
|
+
"GenericInverter",
|
|
21
|
+
"HybridInverter",
|
|
22
|
+
# Feature detection
|
|
23
|
+
"InverterFamily",
|
|
24
|
+
"InverterFeatures",
|
|
25
|
+
"InverterModelInfo",
|
|
26
|
+
"GridType",
|
|
27
|
+
# Feature utilities
|
|
28
|
+
"get_inverter_family",
|
|
29
|
+
"get_family_features",
|
|
30
|
+
"DEVICE_TYPE_CODE_TO_FAMILY",
|
|
31
|
+
"FAMILY_DEFAULT_FEATURES",
|
|
32
|
+
]
|