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,589 @@
|
|
|
1
|
+
"""Battery module for individual battery monitoring.
|
|
2
|
+
|
|
3
|
+
This module provides the Battery class for monitoring individual battery modules
|
|
4
|
+
within an inverter's battery array.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pylxpweb.constants import get_battery_field_precision, scale_battery_value
|
|
12
|
+
|
|
13
|
+
from .base import BaseDevice
|
|
14
|
+
from .models import DeviceClass, DeviceInfo, Entity, StateClass
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pylxpweb import LuxpowerClient
|
|
18
|
+
from pylxpweb.models import BatteryModule
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Battery(BaseDevice):
|
|
22
|
+
"""Represents an individual battery module.
|
|
23
|
+
|
|
24
|
+
Each inverter can have multiple battery modules, each with independent monitoring
|
|
25
|
+
of voltage, current, SoC, SoH, temperature, and cell voltages.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
# Battery is typically created from BatteryInfo API response
|
|
30
|
+
battery_info = await client.api.batteries.get_battery_info(serial_num)
|
|
31
|
+
|
|
32
|
+
for battery_data in battery_info.batteryArray:
|
|
33
|
+
battery = Battery(client=client, battery_data=battery_data)
|
|
34
|
+
print(f"Battery {battery.battery_index}: {battery.soc}% SOC")
|
|
35
|
+
print(f"Voltage: {battery.voltage}V, Current: {battery.current}A")
|
|
36
|
+
print(f"Cell voltage delta: {battery.cell_voltage_delta}V")
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: LuxpowerClient, battery_data: BatteryModule) -> None:
|
|
41
|
+
"""Initialize battery module.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client: LuxpowerClient instance for API access
|
|
45
|
+
battery_data: BatteryModule data from API
|
|
46
|
+
"""
|
|
47
|
+
# Use batteryKey as serial_number for BaseDevice
|
|
48
|
+
super().__init__(client, battery_data.batteryKey, "Battery Module")
|
|
49
|
+
|
|
50
|
+
self._battery_key = battery_data.batteryKey
|
|
51
|
+
self._battery_sn = battery_data.batterySn
|
|
52
|
+
self._battery_index = battery_data.batIndex
|
|
53
|
+
self._data = battery_data
|
|
54
|
+
|
|
55
|
+
# Public accessors for backward compatibility
|
|
56
|
+
@property
|
|
57
|
+
def battery_key(self) -> str:
|
|
58
|
+
"""Get battery key identifier."""
|
|
59
|
+
return self._battery_key
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def battery_sn(self) -> str:
|
|
63
|
+
"""Get battery serial number."""
|
|
64
|
+
return self._battery_sn
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def battery_index(self) -> int:
|
|
68
|
+
"""Get battery index position."""
|
|
69
|
+
return self._battery_index
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def data(self) -> BatteryModule:
|
|
73
|
+
"""Get raw battery data.
|
|
74
|
+
|
|
75
|
+
Note: Direct modification of this data is discouraged.
|
|
76
|
+
Use the provided setter methods or properties instead.
|
|
77
|
+
"""
|
|
78
|
+
return self._data
|
|
79
|
+
|
|
80
|
+
@data.setter
|
|
81
|
+
def data(self, value: BatteryModule) -> None:
|
|
82
|
+
"""Set raw battery data.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
value: New BatteryModule data
|
|
86
|
+
"""
|
|
87
|
+
self._data = value
|
|
88
|
+
|
|
89
|
+
# ========== Identification Properties ==========
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def battery_type(self) -> str | None:
|
|
93
|
+
"""Get battery type identifier.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Battery type string, or None if not available.
|
|
97
|
+
"""
|
|
98
|
+
return self._data.batteryType
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def battery_type_text(self) -> str:
|
|
102
|
+
"""Get battery type display text.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Battery type display text. Falls back to "Lithium" if not available.
|
|
106
|
+
"""
|
|
107
|
+
# If batteryTypeText is empty or None, provide reasonable default
|
|
108
|
+
if not self._data.batteryTypeText:
|
|
109
|
+
# Most EG4/Luxpower batteries are Lithium
|
|
110
|
+
return "Lithium"
|
|
111
|
+
return self._data.batteryTypeText
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def bms_model(self) -> str | None:
|
|
115
|
+
"""Get BMS model information.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
BMS model text, or None if not available.
|
|
119
|
+
"""
|
|
120
|
+
return self._data.batBmsModelText
|
|
121
|
+
|
|
122
|
+
# ========== Status Properties ==========
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def is_lost(self) -> bool:
|
|
126
|
+
"""Check if battery communication is lost.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if battery is not communicating.
|
|
130
|
+
"""
|
|
131
|
+
return self._data.lost
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def last_update_time(self) -> str | None:
|
|
135
|
+
"""Get last update timestamp.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Last update timestamp string, or None if not available.
|
|
139
|
+
"""
|
|
140
|
+
return self._data.lastUpdateTime
|
|
141
|
+
|
|
142
|
+
# ========== Voltage and Current ==========
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def voltage(self) -> float:
|
|
146
|
+
"""Get battery voltage in volts.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Battery voltage (scaled from totalVoltage ÷100).
|
|
150
|
+
"""
|
|
151
|
+
return scale_battery_value("totalVoltage", self._data.totalVoltage)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def current(self) -> float:
|
|
155
|
+
"""Get battery current in amps.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Battery current (scaled from current ÷10). **CRITICAL: Not ÷100**
|
|
159
|
+
"""
|
|
160
|
+
return scale_battery_value("current", self._data.current)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def power(self) -> float:
|
|
164
|
+
"""Get battery power in watts (calculated from V * I).
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Battery power in watts, rounded to voltage precision.
|
|
168
|
+
"""
|
|
169
|
+
# Use voltage precision (2 decimals) as it's higher than current (1 decimal)
|
|
170
|
+
precision = get_battery_field_precision("totalVoltage")
|
|
171
|
+
return round(self.voltage * self.current, precision)
|
|
172
|
+
|
|
173
|
+
# ========== State of Charge/Health ==========
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def soc(self) -> int:
|
|
177
|
+
"""Get battery state of charge.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
State of charge percentage (0-100).
|
|
181
|
+
"""
|
|
182
|
+
return self._data.soc
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def soh(self) -> int:
|
|
186
|
+
"""Get battery state of health.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
State of health percentage (0-100).
|
|
190
|
+
"""
|
|
191
|
+
return self._data.soh
|
|
192
|
+
|
|
193
|
+
# ========== Capacity Properties ==========
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def current_remain_capacity(self) -> int:
|
|
197
|
+
"""Get current remaining capacity in amp-hours.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Current remaining capacity in Ah.
|
|
201
|
+
"""
|
|
202
|
+
return self._data.currentRemainCapacity
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def current_full_capacity(self) -> int:
|
|
206
|
+
"""Get current full capacity in amp-hours.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Current full capacity in Ah.
|
|
210
|
+
"""
|
|
211
|
+
return self._data.currentFullCapacity
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def capacity_percent(self) -> int:
|
|
215
|
+
"""Get current capacity as percentage of full capacity.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Capacity percentage (0-100). If not provided by API, calculates
|
|
219
|
+
from currentRemainCapacity / currentFullCapacity and rounds to
|
|
220
|
+
nearest integer.
|
|
221
|
+
"""
|
|
222
|
+
# Use API value if available
|
|
223
|
+
if self._data.currentCapacityPercent is not None:
|
|
224
|
+
return self._data.currentCapacityPercent
|
|
225
|
+
|
|
226
|
+
# Calculate from remain/full capacity, rounded to nearest integer
|
|
227
|
+
if self._data.currentFullCapacity > 0:
|
|
228
|
+
return round((self._data.currentRemainCapacity / self._data.currentFullCapacity) * 100)
|
|
229
|
+
|
|
230
|
+
# Fallback to 0 if full capacity is 0
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
# ========== Temperature Properties ==========
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def max_cell_temp(self) -> float:
|
|
237
|
+
"""Get maximum cell temperature in Celsius.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Maximum cell temperature (scaled from batMaxCellTemp ÷10).
|
|
241
|
+
"""
|
|
242
|
+
return scale_battery_value("batMaxCellTemp", self._data.batMaxCellTemp)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def min_cell_temp(self) -> float:
|
|
246
|
+
"""Get minimum cell temperature in Celsius.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Minimum cell temperature (scaled from batMinCellTemp ÷10).
|
|
250
|
+
"""
|
|
251
|
+
return scale_battery_value("batMinCellTemp", self._data.batMinCellTemp)
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def max_cell_temp_num(self) -> int | None:
|
|
255
|
+
"""Get cell number with maximum temperature.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Cell number (0-indexed), or None if not available.
|
|
259
|
+
"""
|
|
260
|
+
return self._data.batMaxCellNumTemp
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def min_cell_temp_num(self) -> int | None:
|
|
264
|
+
"""Get cell number with minimum temperature.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Cell number (0-indexed), or None if not available.
|
|
268
|
+
"""
|
|
269
|
+
return self._data.batMinCellNumTemp
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def cell_temp_delta(self) -> float:
|
|
273
|
+
"""Get cell temperature imbalance (max - min).
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Temperature difference between hottest and coolest cell in Celsius,
|
|
277
|
+
rounded to source data precision.
|
|
278
|
+
"""
|
|
279
|
+
precision = get_battery_field_precision("batMaxCellTemp")
|
|
280
|
+
return round(self.max_cell_temp - self.min_cell_temp, precision)
|
|
281
|
+
|
|
282
|
+
# ========== Cell Voltage Properties ==========
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def max_cell_voltage(self) -> float:
|
|
286
|
+
"""Get maximum cell voltage in volts.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Maximum cell voltage (scaled from batMaxCellVoltage ÷1000).
|
|
290
|
+
"""
|
|
291
|
+
return scale_battery_value("batMaxCellVoltage", self._data.batMaxCellVoltage)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def min_cell_voltage(self) -> float:
|
|
295
|
+
"""Get minimum cell voltage in volts.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Minimum cell voltage (scaled from batMinCellVoltage ÷1000).
|
|
299
|
+
"""
|
|
300
|
+
return scale_battery_value("batMinCellVoltage", self._data.batMinCellVoltage)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def max_cell_voltage_num(self) -> int | None:
|
|
304
|
+
"""Get cell number with maximum voltage.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Cell number (0-indexed), or None if not available.
|
|
308
|
+
"""
|
|
309
|
+
return self._data.batMaxCellNumVolt
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def min_cell_voltage_num(self) -> int | None:
|
|
313
|
+
"""Get cell number with minimum voltage.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Cell number (0-indexed), or None if not available.
|
|
317
|
+
"""
|
|
318
|
+
return self._data.batMinCellNumVolt
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def cell_voltage_delta(self) -> float:
|
|
322
|
+
"""Get cell voltage imbalance (max - min).
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Voltage difference between highest and lowest cell in volts,
|
|
326
|
+
rounded to source data precision.
|
|
327
|
+
"""
|
|
328
|
+
precision = get_battery_field_precision("batMaxCellVoltage")
|
|
329
|
+
return round(self.max_cell_voltage - self.min_cell_voltage, precision)
|
|
330
|
+
|
|
331
|
+
# ========== Charge Parameters ==========
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def charge_max_current(self) -> float | None:
|
|
335
|
+
"""Get maximum charge current setting in amps.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Maximum charge current (÷100 for amps), or None if not available.
|
|
339
|
+
"""
|
|
340
|
+
if self._data.batChargeMaxCur is None:
|
|
341
|
+
return None
|
|
342
|
+
return scale_battery_value("batChargeMaxCur", self._data.batChargeMaxCur)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def charge_voltage_ref(self) -> float | None:
|
|
346
|
+
"""Get charge voltage reference setting in volts.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Charge voltage reference (÷10 for volts), or None if not available.
|
|
350
|
+
"""
|
|
351
|
+
if self._data.batChargeVoltRef is None:
|
|
352
|
+
return None
|
|
353
|
+
return scale_battery_value("batChargeVoltRef", self._data.batChargeVoltRef)
|
|
354
|
+
|
|
355
|
+
# ========== Cycle Count and Firmware ==========
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def cycle_count(self) -> int:
|
|
359
|
+
"""Get battery cycle count.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Number of charge/discharge cycles.
|
|
363
|
+
"""
|
|
364
|
+
return self._data.cycleCnt
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def firmware_version(self) -> str:
|
|
368
|
+
"""Get battery firmware version.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Firmware version string.
|
|
372
|
+
"""
|
|
373
|
+
return self._data.fwVersionText
|
|
374
|
+
|
|
375
|
+
# ========== Additional Metrics ==========
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def charge_capacity(self) -> str | None:
|
|
379
|
+
"""Get charge capacity metric.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Charge capacity string, or None if not available.
|
|
383
|
+
"""
|
|
384
|
+
return self._data.chgCapacity
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def discharge_capacity(self) -> str | None:
|
|
388
|
+
"""Get discharge capacity metric.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Discharge capacity string, or None if not available.
|
|
392
|
+
"""
|
|
393
|
+
return self._data.disChgCapacity
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def ambient_temp(self) -> str | None:
|
|
397
|
+
"""Get ambient temperature.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Ambient temperature string, or None if not available.
|
|
401
|
+
"""
|
|
402
|
+
return self._data.ambientTemp
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def mos_temp(self) -> str | None:
|
|
406
|
+
"""Get MOSFET temperature.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
MOSFET temperature string, or None if not available.
|
|
410
|
+
"""
|
|
411
|
+
return self._data.mosTemp
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def notice_info(self) -> str | None:
|
|
415
|
+
"""Get notice/warning information.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Notice information string, or None if not available.
|
|
419
|
+
"""
|
|
420
|
+
return self._data.noticeInfo
|
|
421
|
+
|
|
422
|
+
async def refresh(self) -> None:
|
|
423
|
+
"""Refresh battery data.
|
|
424
|
+
|
|
425
|
+
Note: Battery data is refreshed through the parent inverter.
|
|
426
|
+
This method is a no-op for individual batteries.
|
|
427
|
+
"""
|
|
428
|
+
# Battery data comes from inverter's getBatteryInfo call
|
|
429
|
+
# Individual batteries don't have their own refresh endpoint
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
def to_device_info(self) -> DeviceInfo:
|
|
433
|
+
"""Convert to device info model.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
DeviceInfo with battery metadata.
|
|
437
|
+
"""
|
|
438
|
+
return DeviceInfo(
|
|
439
|
+
identifiers={("pylxpweb", f"battery_{self._battery_key}")},
|
|
440
|
+
name=f"Battery {self._battery_index + 1} ({self._battery_sn})",
|
|
441
|
+
manufacturer="EG4/Luxpower",
|
|
442
|
+
model="Battery Module",
|
|
443
|
+
sw_version=self.firmware_version,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def to_entities(self) -> list[Entity]:
|
|
447
|
+
"""Generate entities for this battery.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
List of Entity objects representing sensors for this battery.
|
|
451
|
+
"""
|
|
452
|
+
entities = []
|
|
453
|
+
# Use properties for consistent access
|
|
454
|
+
battery_key = self.battery_key
|
|
455
|
+
battery_num = self.battery_index + 1
|
|
456
|
+
|
|
457
|
+
# Voltage
|
|
458
|
+
entities.append(
|
|
459
|
+
Entity(
|
|
460
|
+
unique_id=f"{battery_key}_voltage",
|
|
461
|
+
name=f"Battery {battery_num} Voltage",
|
|
462
|
+
device_class=DeviceClass.VOLTAGE,
|
|
463
|
+
state_class=StateClass.MEASUREMENT,
|
|
464
|
+
unit_of_measurement="V",
|
|
465
|
+
value=self.voltage,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Current
|
|
470
|
+
entities.append(
|
|
471
|
+
Entity(
|
|
472
|
+
unique_id=f"{battery_key}_current",
|
|
473
|
+
name=f"Battery {battery_num} Current",
|
|
474
|
+
device_class=DeviceClass.CURRENT,
|
|
475
|
+
state_class=StateClass.MEASUREMENT,
|
|
476
|
+
unit_of_measurement="A",
|
|
477
|
+
value=self.current,
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Power
|
|
482
|
+
entities.append(
|
|
483
|
+
Entity(
|
|
484
|
+
unique_id=f"{battery_key}_power",
|
|
485
|
+
name=f"Battery {battery_num} Power",
|
|
486
|
+
device_class=DeviceClass.POWER,
|
|
487
|
+
state_class=StateClass.MEASUREMENT,
|
|
488
|
+
unit_of_measurement="W",
|
|
489
|
+
value=self.power,
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# State of Charge
|
|
494
|
+
entities.append(
|
|
495
|
+
Entity(
|
|
496
|
+
unique_id=f"{battery_key}_soc",
|
|
497
|
+
name=f"Battery {battery_num} SOC",
|
|
498
|
+
device_class=DeviceClass.BATTERY,
|
|
499
|
+
state_class=StateClass.MEASUREMENT,
|
|
500
|
+
unit_of_measurement="%",
|
|
501
|
+
value=self.soc,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# State of Health
|
|
506
|
+
entities.append(
|
|
507
|
+
Entity(
|
|
508
|
+
unique_id=f"{battery_key}_soh",
|
|
509
|
+
name=f"Battery {battery_num} SOH",
|
|
510
|
+
device_class=DeviceClass.BATTERY,
|
|
511
|
+
state_class=StateClass.MEASUREMENT,
|
|
512
|
+
unit_of_measurement="%",
|
|
513
|
+
value=self.soh,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Maximum Cell Temperature
|
|
518
|
+
entities.append(
|
|
519
|
+
Entity(
|
|
520
|
+
unique_id=f"{battery_key}_max_cell_temp",
|
|
521
|
+
name=f"Battery {battery_num} Max Cell Temperature",
|
|
522
|
+
device_class=DeviceClass.TEMPERATURE,
|
|
523
|
+
state_class=StateClass.MEASUREMENT,
|
|
524
|
+
unit_of_measurement="°C",
|
|
525
|
+
value=self.max_cell_temp,
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Minimum Cell Temperature
|
|
530
|
+
entities.append(
|
|
531
|
+
Entity(
|
|
532
|
+
unique_id=f"{battery_key}_min_cell_temp",
|
|
533
|
+
name=f"Battery {battery_num} Min Cell Temperature",
|
|
534
|
+
device_class=DeviceClass.TEMPERATURE,
|
|
535
|
+
state_class=StateClass.MEASUREMENT,
|
|
536
|
+
unit_of_measurement="°C",
|
|
537
|
+
value=self.min_cell_temp,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Maximum Cell Voltage
|
|
542
|
+
entities.append(
|
|
543
|
+
Entity(
|
|
544
|
+
unique_id=f"{battery_key}_max_cell_voltage",
|
|
545
|
+
name=f"Battery {battery_num} Max Cell Voltage",
|
|
546
|
+
device_class=DeviceClass.VOLTAGE,
|
|
547
|
+
state_class=StateClass.MEASUREMENT,
|
|
548
|
+
unit_of_measurement="V",
|
|
549
|
+
value=self.max_cell_voltage,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Minimum Cell Voltage
|
|
554
|
+
entities.append(
|
|
555
|
+
Entity(
|
|
556
|
+
unique_id=f"{battery_key}_min_cell_voltage",
|
|
557
|
+
name=f"Battery {battery_num} Min Cell Voltage",
|
|
558
|
+
device_class=DeviceClass.VOLTAGE,
|
|
559
|
+
state_class=StateClass.MEASUREMENT,
|
|
560
|
+
unit_of_measurement="V",
|
|
561
|
+
value=self.min_cell_voltage,
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Cell Voltage Delta (imbalance indicator)
|
|
566
|
+
entities.append(
|
|
567
|
+
Entity(
|
|
568
|
+
unique_id=f"{battery_key}_cell_voltage_delta",
|
|
569
|
+
name=f"Battery {battery_num} Cell Voltage Delta",
|
|
570
|
+
device_class=DeviceClass.VOLTAGE,
|
|
571
|
+
state_class=StateClass.MEASUREMENT,
|
|
572
|
+
unit_of_measurement="V",
|
|
573
|
+
value=self.cell_voltage_delta,
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Cycle Count
|
|
578
|
+
entities.append(
|
|
579
|
+
Entity(
|
|
580
|
+
unique_id=f"{battery_key}_cycle_count",
|
|
581
|
+
name=f"Battery {battery_num} Cycle Count",
|
|
582
|
+
device_class=None, # No standard device class for cycle count
|
|
583
|
+
state_class=StateClass.TOTAL_INCREASING,
|
|
584
|
+
unit_of_measurement="cycles",
|
|
585
|
+
value=self.cycle_count,
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return entities
|