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.
Files changed (46) hide show
  1. pylxpweb/__init__.py +47 -2
  2. pylxpweb/api_namespace.py +241 -0
  3. pylxpweb/cli/__init__.py +3 -0
  4. pylxpweb/cli/collect_device_data.py +874 -0
  5. pylxpweb/client.py +387 -26
  6. pylxpweb/constants/__init__.py +481 -0
  7. pylxpweb/constants/api.py +48 -0
  8. pylxpweb/constants/devices.py +98 -0
  9. pylxpweb/constants/locations.py +227 -0
  10. pylxpweb/{constants.py → constants/registers.py} +72 -238
  11. pylxpweb/constants/scaling.py +479 -0
  12. pylxpweb/devices/__init__.py +32 -0
  13. pylxpweb/devices/_firmware_update_mixin.py +504 -0
  14. pylxpweb/devices/_mid_runtime_properties.py +545 -0
  15. pylxpweb/devices/base.py +122 -0
  16. pylxpweb/devices/battery.py +589 -0
  17. pylxpweb/devices/battery_bank.py +331 -0
  18. pylxpweb/devices/inverters/__init__.py +32 -0
  19. pylxpweb/devices/inverters/_features.py +378 -0
  20. pylxpweb/devices/inverters/_runtime_properties.py +596 -0
  21. pylxpweb/devices/inverters/base.py +2124 -0
  22. pylxpweb/devices/inverters/generic.py +192 -0
  23. pylxpweb/devices/inverters/hybrid.py +274 -0
  24. pylxpweb/devices/mid_device.py +183 -0
  25. pylxpweb/devices/models.py +126 -0
  26. pylxpweb/devices/parallel_group.py +351 -0
  27. pylxpweb/devices/station.py +908 -0
  28. pylxpweb/endpoints/control.py +980 -2
  29. pylxpweb/endpoints/devices.py +249 -16
  30. pylxpweb/endpoints/firmware.py +43 -10
  31. pylxpweb/endpoints/plants.py +15 -19
  32. pylxpweb/exceptions.py +4 -0
  33. pylxpweb/models.py +629 -40
  34. pylxpweb/transports/__init__.py +78 -0
  35. pylxpweb/transports/capabilities.py +101 -0
  36. pylxpweb/transports/data.py +495 -0
  37. pylxpweb/transports/exceptions.py +59 -0
  38. pylxpweb/transports/factory.py +119 -0
  39. pylxpweb/transports/http.py +329 -0
  40. pylxpweb/transports/modbus.py +557 -0
  41. pylxpweb/transports/protocol.py +217 -0
  42. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/METADATA +130 -85
  43. pylxpweb-0.5.0.dist-info/RECORD +52 -0
  44. {pylxpweb-0.1.0.dist-info → pylxpweb-0.5.0.dist-info}/WHEEL +1 -1
  45. pylxpweb-0.5.0.dist-info/entry_points.txt +3 -0
  46. 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