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,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
|
pylxpweb/devices/base.py
ADDED
|
@@ -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
|
+
...
|