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,545 @@
1
+ """Runtime properties mixin for MIDDevice (GridBOSS).
2
+
3
+ This mixin provides properly-scaled property accessors for all GridBOSS
4
+ sensor data from the MID device runtime API. All properties return typed,
5
+ scaled values with graceful None handling.
6
+
7
+ Properties are organized by category:
8
+ - Voltage Properties (Grid, UPS, Generator - aggregate and per-phase)
9
+ - Current Properties (Grid, Load, Generator, UPS - per-phase)
10
+ - Power Properties (Grid, Load, Generator, UPS - per-phase and totals)
11
+ - Frequency Properties
12
+ - Smart Port Status
13
+ - System Status & Info
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ from pylxpweb.constants import scale_mid_frequency, scale_mid_voltage
21
+
22
+ if TYPE_CHECKING:
23
+ from pylxpweb.models import MidboxRuntime
24
+
25
+
26
+ class MIDRuntimePropertiesMixin:
27
+ """Mixin providing runtime property accessors for MID devices."""
28
+
29
+ _runtime: MidboxRuntime | None
30
+
31
+ # ===========================================
32
+ # Voltage Properties - Aggregate
33
+ # ===========================================
34
+
35
+ @property
36
+ def grid_voltage(self) -> float:
37
+ """Get aggregate grid voltage in volts.
38
+
39
+ Returns:
40
+ Grid RMS voltage (÷10), or 0.0 if no data.
41
+ """
42
+ if self._runtime is None:
43
+ return 0.0
44
+
45
+ return scale_mid_voltage(self._runtime.midboxData.gridRmsVolt)
46
+
47
+ @property
48
+ def ups_voltage(self) -> float:
49
+ """Get aggregate UPS voltage in volts.
50
+
51
+ Returns:
52
+ UPS RMS voltage (÷10), or 0.0 if no data.
53
+ """
54
+ if self._runtime is None:
55
+ return 0.0
56
+
57
+ return scale_mid_voltage(self._runtime.midboxData.upsRmsVolt)
58
+
59
+ @property
60
+ def generator_voltage(self) -> float:
61
+ """Get aggregate generator voltage in volts.
62
+
63
+ Returns:
64
+ Generator RMS voltage (÷10), or 0.0 if no data.
65
+ """
66
+ if self._runtime is None:
67
+ return 0.0
68
+
69
+ return scale_mid_voltage(self._runtime.midboxData.genRmsVolt)
70
+
71
+ # ===========================================
72
+ # Voltage Properties - Grid Per-Phase
73
+ # ===========================================
74
+
75
+ @property
76
+ def grid_l1_voltage(self) -> float:
77
+ """Get grid L1 voltage in volts.
78
+
79
+ Returns:
80
+ Grid L1 RMS voltage (÷10), or 0.0 if no data.
81
+ """
82
+ if self._runtime is None:
83
+ return 0.0
84
+
85
+ return scale_mid_voltage(self._runtime.midboxData.gridL1RmsVolt)
86
+
87
+ @property
88
+ def grid_l2_voltage(self) -> float:
89
+ """Get grid L2 voltage in volts.
90
+
91
+ Returns:
92
+ Grid L2 RMS voltage (÷10), or 0.0 if no data.
93
+ """
94
+ if self._runtime is None:
95
+ return 0.0
96
+
97
+ return scale_mid_voltage(self._runtime.midboxData.gridL2RmsVolt)
98
+
99
+ # ===========================================
100
+ # Voltage Properties - UPS Per-Phase
101
+ # ===========================================
102
+
103
+ @property
104
+ def ups_l1_voltage(self) -> float:
105
+ """Get UPS L1 voltage in volts.
106
+
107
+ Returns:
108
+ UPS L1 RMS voltage (÷10), or 0.0 if no data.
109
+ """
110
+ if self._runtime is None:
111
+ return 0.0
112
+
113
+ return scale_mid_voltage(self._runtime.midboxData.upsL1RmsVolt)
114
+
115
+ @property
116
+ def ups_l2_voltage(self) -> float:
117
+ """Get UPS L2 voltage in volts.
118
+
119
+ Returns:
120
+ UPS L2 RMS voltage (÷10), or 0.0 if no data.
121
+ """
122
+ if self._runtime is None:
123
+ return 0.0
124
+
125
+ return scale_mid_voltage(self._runtime.midboxData.upsL2RmsVolt)
126
+
127
+ # ===========================================
128
+ # Voltage Properties - Generator Per-Phase
129
+ # ===========================================
130
+
131
+ @property
132
+ def generator_l1_voltage(self) -> float:
133
+ """Get generator L1 voltage in volts.
134
+
135
+ Returns:
136
+ Generator L1 RMS voltage (÷10), or 0.0 if no data.
137
+ """
138
+ if self._runtime is None:
139
+ return 0.0
140
+
141
+ return scale_mid_voltage(self._runtime.midboxData.genL1RmsVolt)
142
+
143
+ @property
144
+ def generator_l2_voltage(self) -> float:
145
+ """Get generator L2 voltage in volts.
146
+
147
+ Returns:
148
+ Generator L2 RMS voltage (÷10), or 0.0 if no data.
149
+ """
150
+ if self._runtime is None:
151
+ return 0.0
152
+
153
+ return scale_mid_voltage(self._runtime.midboxData.genL2RmsVolt)
154
+
155
+ # ===========================================
156
+ # Current Properties - Grid
157
+ # ===========================================
158
+
159
+ @property
160
+ def grid_l1_current(self) -> float:
161
+ """Get grid L1 current in amps.
162
+
163
+ Returns:
164
+ Grid L1 RMS current (÷100), or 0.0 if no data.
165
+ """
166
+ if self._runtime is None:
167
+ return 0.0
168
+ return self._runtime.midboxData.gridL1RmsCurr / 100.0
169
+
170
+ @property
171
+ def grid_l2_current(self) -> float:
172
+ """Get grid L2 current in amps.
173
+
174
+ Returns:
175
+ Grid L2 RMS current (÷100), or 0.0 if no data.
176
+ """
177
+ if self._runtime is None:
178
+ return 0.0
179
+ return self._runtime.midboxData.gridL2RmsCurr / 100.0
180
+
181
+ # ===========================================
182
+ # Current Properties - Load
183
+ # ===========================================
184
+
185
+ @property
186
+ def load_l1_current(self) -> float:
187
+ """Get load L1 current in amps.
188
+
189
+ Returns:
190
+ Load L1 RMS current (÷100), or 0.0 if no data.
191
+ """
192
+ if self._runtime is None:
193
+ return 0.0
194
+ return self._runtime.midboxData.loadL1RmsCurr / 100.0
195
+
196
+ @property
197
+ def load_l2_current(self) -> float:
198
+ """Get load L2 current in amps.
199
+
200
+ Returns:
201
+ Load L2 RMS current (÷100), or 0.0 if no data.
202
+ """
203
+ if self._runtime is None:
204
+ return 0.0
205
+ return self._runtime.midboxData.loadL2RmsCurr / 100.0
206
+
207
+ # ===========================================
208
+ # Current Properties - Generator
209
+ # ===========================================
210
+
211
+ @property
212
+ def generator_l1_current(self) -> float:
213
+ """Get generator L1 current in amps.
214
+
215
+ Returns:
216
+ Generator L1 RMS current (÷100), or 0.0 if no data.
217
+ """
218
+ if self._runtime is None:
219
+ return 0.0
220
+ return self._runtime.midboxData.genL1RmsCurr / 100.0
221
+
222
+ @property
223
+ def generator_l2_current(self) -> float:
224
+ """Get generator L2 current in amps.
225
+
226
+ Returns:
227
+ Generator L2 RMS current (÷100), or 0.0 if no data.
228
+ """
229
+ if self._runtime is None:
230
+ return 0.0
231
+ return self._runtime.midboxData.genL2RmsCurr / 100.0
232
+
233
+ # ===========================================
234
+ # Current Properties - UPS
235
+ # ===========================================
236
+
237
+ @property
238
+ def ups_l1_current(self) -> float:
239
+ """Get UPS L1 current in amps.
240
+
241
+ Returns:
242
+ UPS L1 RMS current (÷100), or 0.0 if no data.
243
+ """
244
+ if self._runtime is None:
245
+ return 0.0
246
+ return self._runtime.midboxData.upsL1RmsCurr / 100.0
247
+
248
+ @property
249
+ def ups_l2_current(self) -> float:
250
+ """Get UPS L2 current in amps.
251
+
252
+ Returns:
253
+ UPS L2 RMS current (÷100), or 0.0 if no data.
254
+ """
255
+ if self._runtime is None:
256
+ return 0.0
257
+ return self._runtime.midboxData.upsL2RmsCurr / 100.0
258
+
259
+ # ===========================================
260
+ # Power Properties - Grid
261
+ # ===========================================
262
+
263
+ @property
264
+ def grid_l1_power(self) -> int:
265
+ """Get grid L1 active power in watts.
266
+
267
+ Returns:
268
+ Grid L1 power, or 0 if no data.
269
+ """
270
+ if self._runtime is None:
271
+ return 0
272
+ return self._runtime.midboxData.gridL1ActivePower
273
+
274
+ @property
275
+ def grid_l2_power(self) -> int:
276
+ """Get grid L2 active power in watts.
277
+
278
+ Returns:
279
+ Grid L2 power, or 0 if no data.
280
+ """
281
+ if self._runtime is None:
282
+ return 0
283
+ return self._runtime.midboxData.gridL2ActivePower
284
+
285
+ @property
286
+ def grid_power(self) -> int:
287
+ """Get total grid power in watts (L1 + L2).
288
+
289
+ Returns:
290
+ Total grid power, or 0 if no data.
291
+ """
292
+ if self._runtime is None:
293
+ return 0
294
+ return (
295
+ self._runtime.midboxData.gridL1ActivePower + self._runtime.midboxData.gridL2ActivePower
296
+ )
297
+
298
+ # ===========================================
299
+ # Power Properties - Load
300
+ # ===========================================
301
+
302
+ @property
303
+ def load_l1_power(self) -> int:
304
+ """Get load L1 active power in watts.
305
+
306
+ Returns:
307
+ Load L1 power, or 0 if no data.
308
+ """
309
+ if self._runtime is None:
310
+ return 0
311
+ return self._runtime.midboxData.loadL1ActivePower
312
+
313
+ @property
314
+ def load_l2_power(self) -> int:
315
+ """Get load L2 active power in watts.
316
+
317
+ Returns:
318
+ Load L2 power, or 0 if no data.
319
+ """
320
+ if self._runtime is None:
321
+ return 0
322
+ return self._runtime.midboxData.loadL2ActivePower
323
+
324
+ @property
325
+ def load_power(self) -> int:
326
+ """Get total load power in watts (L1 + L2).
327
+
328
+ Returns:
329
+ Total load power, or 0 if no data.
330
+ """
331
+ if self._runtime is None:
332
+ return 0
333
+ return (
334
+ self._runtime.midboxData.loadL1ActivePower + self._runtime.midboxData.loadL2ActivePower
335
+ )
336
+
337
+ # ===========================================
338
+ # Power Properties - Generator
339
+ # ===========================================
340
+
341
+ @property
342
+ def generator_l1_power(self) -> int:
343
+ """Get generator L1 active power in watts.
344
+
345
+ Returns:
346
+ Generator L1 power, or 0 if no data.
347
+ """
348
+ if self._runtime is None:
349
+ return 0
350
+ return self._runtime.midboxData.genL1ActivePower
351
+
352
+ @property
353
+ def generator_l2_power(self) -> int:
354
+ """Get generator L2 active power in watts.
355
+
356
+ Returns:
357
+ Generator L2 power, or 0 if no data.
358
+ """
359
+ if self._runtime is None:
360
+ return 0
361
+ return self._runtime.midboxData.genL2ActivePower
362
+
363
+ @property
364
+ def generator_power(self) -> int:
365
+ """Get total generator power in watts (L1 + L2).
366
+
367
+ Returns:
368
+ Total generator power, or 0 if no data.
369
+ """
370
+ if self._runtime is None:
371
+ return 0
372
+ return self._runtime.midboxData.genL1ActivePower + self._runtime.midboxData.genL2ActivePower
373
+
374
+ # ===========================================
375
+ # Power Properties - UPS
376
+ # ===========================================
377
+
378
+ @property
379
+ def ups_l1_power(self) -> int:
380
+ """Get UPS L1 active power in watts.
381
+
382
+ Returns:
383
+ UPS L1 power, or 0 if no data.
384
+ """
385
+ if self._runtime is None:
386
+ return 0
387
+ return self._runtime.midboxData.upsL1ActivePower
388
+
389
+ @property
390
+ def ups_l2_power(self) -> int:
391
+ """Get UPS L2 active power in watts.
392
+
393
+ Returns:
394
+ UPS L2 power, or 0 if no data.
395
+ """
396
+ if self._runtime is None:
397
+ return 0
398
+ return self._runtime.midboxData.upsL2ActivePower
399
+
400
+ @property
401
+ def ups_power(self) -> int:
402
+ """Get total UPS power in watts (L1 + L2).
403
+
404
+ Returns:
405
+ Total UPS power, or 0 if no data.
406
+ """
407
+ if self._runtime is None:
408
+ return 0
409
+ return self._runtime.midboxData.upsL1ActivePower + self._runtime.midboxData.upsL2ActivePower
410
+
411
+ # ===========================================
412
+ # Power Properties - Hybrid System
413
+ # ===========================================
414
+
415
+ @property
416
+ def hybrid_power(self) -> int:
417
+ """Get hybrid system power in watts.
418
+
419
+ Returns:
420
+ Hybrid power (combined system power), or 0 if no data.
421
+ """
422
+ if self._runtime is None:
423
+ return 0
424
+ return self._runtime.midboxData.hybridPower
425
+
426
+ # ===========================================
427
+ # Frequency Properties
428
+ # ===========================================
429
+
430
+ @property
431
+ def grid_frequency(self) -> float:
432
+ """Get grid frequency in Hz.
433
+
434
+ Returns:
435
+ Grid frequency (÷100), or 0.0 if no data.
436
+ """
437
+ if self._runtime is None:
438
+ return 0.0
439
+
440
+ return scale_mid_frequency(self._runtime.midboxData.gridFreq)
441
+
442
+ # ===========================================
443
+ # Smart Port Status
444
+ # ===========================================
445
+
446
+ @property
447
+ def smart_port1_status(self) -> int:
448
+ """Get smart port 1 status.
449
+
450
+ Returns:
451
+ Port 1 status code, or 0 if no data.
452
+ """
453
+ if self._runtime is None:
454
+ return 0
455
+ return self._runtime.midboxData.smartPort1Status
456
+
457
+ @property
458
+ def smart_port2_status(self) -> int:
459
+ """Get smart port 2 status.
460
+
461
+ Returns:
462
+ Port 2 status code, or 0 if no data.
463
+ """
464
+ if self._runtime is None:
465
+ return 0
466
+ return self._runtime.midboxData.smartPort2Status
467
+
468
+ @property
469
+ def smart_port3_status(self) -> int:
470
+ """Get smart port 3 status.
471
+
472
+ Returns:
473
+ Port 3 status code, or 0 if no data.
474
+ """
475
+ if self._runtime is None:
476
+ return 0
477
+ return self._runtime.midboxData.smartPort3Status
478
+
479
+ @property
480
+ def smart_port4_status(self) -> int:
481
+ """Get smart port 4 status.
482
+
483
+ Returns:
484
+ Port 4 status code, or 0 if no data.
485
+ """
486
+ if self._runtime is None:
487
+ return 0
488
+ return self._runtime.midboxData.smartPort4Status
489
+
490
+ # ===========================================
491
+ # System Status & Info
492
+ # ===========================================
493
+
494
+ @property
495
+ def status(self) -> int:
496
+ """Get device status code.
497
+
498
+ Returns:
499
+ Status code, or 0 if no data.
500
+ """
501
+ if self._runtime is None:
502
+ return 0
503
+ return self._runtime.midboxData.status
504
+
505
+ @property
506
+ def server_time(self) -> str:
507
+ """Get server timestamp.
508
+
509
+ Returns:
510
+ Server time string, or empty string if no data.
511
+ """
512
+ if self._runtime is None:
513
+ return ""
514
+ return self._runtime.midboxData.serverTime
515
+
516
+ @property
517
+ def device_time(self) -> str:
518
+ """Get device timestamp.
519
+
520
+ Returns:
521
+ Device time string, or empty string if no data.
522
+ """
523
+ if self._runtime is None:
524
+ return ""
525
+ return self._runtime.midboxData.deviceTime
526
+
527
+ @property
528
+ def firmware_version(self) -> str:
529
+ """Get firmware version.
530
+
531
+ Returns:
532
+ Firmware version string, or empty string if no data.
533
+ """
534
+ if self._runtime is None:
535
+ return ""
536
+ return self._runtime.fwCode
537
+
538
+ @property
539
+ def has_data(self) -> bool:
540
+ """Check if device has runtime data.
541
+
542
+ Returns:
543
+ True if runtime data is available.
544
+ """
545
+ return self._runtime is not None
@@ -0,0 +1,122 @@
1
+ """Base device classes for pylxpweb.
2
+
3
+ This module provides abstract base classes for all device types,
4
+ implementing common functionality like refresh intervals, caching,
5
+ and Home Assistant integration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from datetime import datetime, timedelta
12
+ from typing import TYPE_CHECKING
13
+
14
+ from .models import DeviceInfo, Entity
15
+
16
+ if TYPE_CHECKING:
17
+ from pylxpweb import LuxpowerClient
18
+
19
+
20
+ class BaseDevice(ABC):
21
+ """Abstract base class for all device types.
22
+
23
+ This class provides common functionality for inverters, batteries,
24
+ MID devices, and stations, including:
25
+ - Refresh interval management with TTL
26
+ - Client reference for API access
27
+ - Home Assistant integration methods
28
+
29
+ Subclasses must implement:
30
+ - refresh(): Load/reload device data from API
31
+ - to_ha_device_info(): Convert to HA device registry format
32
+ - to_ha_entities(): Generate HA entity list
33
+
34
+ Example:
35
+ ```python
36
+ class MyDevice(BaseDevice):
37
+ async def refresh(self) -> None:
38
+ data = await self._client.api.devices.get_data(self.serial_number)
39
+ self._process_data(data)
40
+ self._last_refresh = datetime.now()
41
+
42
+ def to_device_info(self) -> DeviceInfo:
43
+ return DeviceInfo(
44
+ identifiers={("pylxpweb", f"device_{self.serial_number}")},
45
+ name=f"My Device {self.serial_number}",
46
+ manufacturer="EG4",
47
+ model=self.model,
48
+ )
49
+
50
+ def to_entities(self) -> list[Entity]:
51
+ return [
52
+ Entity(unique_id=f"{self.serial_number}_power", ...)
53
+ ]
54
+ ```
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ client: LuxpowerClient,
60
+ serial_number: str,
61
+ model: str,
62
+ ) -> None:
63
+ """Initialize base device.
64
+
65
+ Args:
66
+ client: LuxpowerClient instance for API access
67
+ serial_number: Device serial number (unique identifier)
68
+ model: Device model name
69
+ """
70
+ self._client = client
71
+ self.serial_number = serial_number
72
+ self._model = model
73
+ self._last_refresh: datetime | None = None
74
+ self._refresh_interval = timedelta(seconds=30)
75
+
76
+ @property
77
+ def model(self) -> str:
78
+ """Get device model name.
79
+
80
+ Returns:
81
+ Device model name, or "Unknown" if not available.
82
+ """
83
+ return self._model if self._model else "Unknown"
84
+
85
+ @property
86
+ def needs_refresh(self) -> bool:
87
+ """Check if device data needs refreshing based on TTL.
88
+
89
+ Returns:
90
+ True if device has never been refreshed or TTL has expired,
91
+ False if data is still fresh.
92
+ """
93
+ if self._last_refresh is None:
94
+ return True
95
+ return datetime.now() - self._last_refresh > self._refresh_interval
96
+
97
+ @abstractmethod
98
+ async def refresh(self) -> None:
99
+ """Refresh device data from API.
100
+
101
+ Subclasses must implement this to load/reload device-specific data.
102
+ Should update self._last_refresh on success.
103
+ """
104
+ ...
105
+
106
+ @abstractmethod
107
+ def to_device_info(self) -> DeviceInfo:
108
+ """Convert device to generic device info model.
109
+
110
+ Returns:
111
+ DeviceInfo instance with device metadata.
112
+ """
113
+ ...
114
+
115
+ @abstractmethod
116
+ def to_entities(self) -> list[Entity]:
117
+ """Generate entities for this device.
118
+
119
+ Returns:
120
+ List of Entity instances (sensors, switches, etc.)
121
+ """
122
+ ...