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
pylxpweb/models.py
CHANGED
|
@@ -12,6 +12,22 @@ from typing import Any
|
|
|
12
12
|
from pydantic import BaseModel, Field, field_serializer
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class OperatingMode(str, Enum):
|
|
16
|
+
"""Inverter operating modes.
|
|
17
|
+
|
|
18
|
+
These are the two valid operating states for an inverter:
|
|
19
|
+
- NORMAL: Normal operation (power on)
|
|
20
|
+
- STANDBY: Standby mode (power off)
|
|
21
|
+
|
|
22
|
+
Note: Quick Charge and Quick Discharge are not operating modes,
|
|
23
|
+
they are function controls (enable/disable) that work alongside
|
|
24
|
+
the operating mode.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
NORMAL = "normal"
|
|
28
|
+
STANDBY = "standby"
|
|
29
|
+
|
|
30
|
+
|
|
15
31
|
def _obfuscate_serial(serial: str) -> str:
|
|
16
32
|
"""Obfuscate serial number, showing only first 2 and last 2 digits."""
|
|
17
33
|
if len(serial) <= 4:
|
|
@@ -43,6 +59,7 @@ class BatteryType(str, Enum):
|
|
|
43
59
|
|
|
44
60
|
LITHIUM = "LITHIUM"
|
|
45
61
|
LEAD_ACID = "LEAD_ACID"
|
|
62
|
+
NO_BATTERY = "NO_BATTERY"
|
|
46
63
|
|
|
47
64
|
|
|
48
65
|
class UserRole(str, Enum):
|
|
@@ -135,23 +152,30 @@ class ParallelGroupBasic(BaseModel):
|
|
|
135
152
|
|
|
136
153
|
|
|
137
154
|
class PlantBasic(BaseModel):
|
|
138
|
-
"""Plant information from login response.
|
|
155
|
+
"""Plant information from login response.
|
|
156
|
+
|
|
157
|
+
Note: parallelGroups may not be present for all device types (e.g., 12000XP).
|
|
158
|
+
"""
|
|
139
159
|
|
|
140
160
|
plantId: int
|
|
141
161
|
name: str
|
|
142
162
|
timezoneHourOffset: int
|
|
143
163
|
timezoneMinuteOffset: int
|
|
144
164
|
inverters: list[InverterBasic]
|
|
145
|
-
parallelGroups: list[ParallelGroupBasic]
|
|
165
|
+
parallelGroups: list[ParallelGroupBasic] = []
|
|
146
166
|
|
|
147
167
|
|
|
148
168
|
class TechInfo(BaseModel):
|
|
149
|
-
"""Technical support information.
|
|
169
|
+
"""Technical support information.
|
|
170
|
+
|
|
171
|
+
Note: techInfoType2/techInfo2 are optional as some regional APIs
|
|
172
|
+
(e.g., EU Luxpower) may only return one tech info item.
|
|
173
|
+
"""
|
|
150
174
|
|
|
151
175
|
techInfoType1: str
|
|
152
176
|
techInfo1: str
|
|
153
|
-
techInfoType2: str
|
|
154
|
-
techInfo2: str
|
|
177
|
+
techInfoType2: str | None = None
|
|
178
|
+
techInfo2: str | None = None
|
|
155
179
|
techInfoCount: int
|
|
156
180
|
|
|
157
181
|
|
|
@@ -189,7 +213,7 @@ class LoginResponse(BaseModel):
|
|
|
189
213
|
tempUnit: str
|
|
190
214
|
tempUnitText: str
|
|
191
215
|
dateFormat: str
|
|
192
|
-
userChartRecord: str
|
|
216
|
+
userChartRecord: str | None = None
|
|
193
217
|
firewallNotificationEnable: str
|
|
194
218
|
userCreateDate: str
|
|
195
219
|
userCreatedDays: int
|
|
@@ -280,39 +304,124 @@ class InverterDevice(BaseModel):
|
|
|
280
304
|
return _obfuscate_serial(value)
|
|
281
305
|
|
|
282
306
|
|
|
283
|
-
class
|
|
284
|
-
"""
|
|
307
|
+
class ParallelGroupDeviceItem(BaseModel):
|
|
308
|
+
"""Device in a parallel group from getParallelGroupDetails endpoint."""
|
|
285
309
|
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
serialNum: str
|
|
311
|
+
deviceType: int
|
|
312
|
+
subDeviceType: int
|
|
313
|
+
phase: int
|
|
314
|
+
dtc: int
|
|
315
|
+
machineType: int
|
|
316
|
+
parallelIndex: str
|
|
317
|
+
parallelNumText: str
|
|
318
|
+
lost: bool
|
|
319
|
+
roleText: str
|
|
320
|
+
# Optional runtime data fields (present for inverters, not for GridBOSS)
|
|
321
|
+
vpv1: int | None = None
|
|
322
|
+
ppv1: int | None = None
|
|
323
|
+
vpv2: int | None = None
|
|
324
|
+
ppv2: int | None = None
|
|
325
|
+
vpv3: int | None = None
|
|
326
|
+
ppv3: int | None = None
|
|
327
|
+
soc: int | None = None
|
|
328
|
+
vBat: int | None = None
|
|
329
|
+
pCharge: int | None = None
|
|
330
|
+
pDisCharge: int | None = None
|
|
331
|
+
peps: int | None = None
|
|
332
|
+
|
|
333
|
+
@field_serializer("serialNum")
|
|
334
|
+
def serialize_serial(self, value: str) -> str:
|
|
335
|
+
"""Obfuscate serial number in serialized output."""
|
|
336
|
+
return _obfuscate_serial(value)
|
|
288
337
|
|
|
289
338
|
|
|
290
339
|
class ParallelGroupDetailsResponse(BaseModel):
|
|
291
|
-
"""Parallel group details API response.
|
|
340
|
+
"""Parallel group details API response.
|
|
341
|
+
|
|
342
|
+
Returns devices in a parallel group including GridBOSS (if present) and inverters.
|
|
343
|
+
"""
|
|
292
344
|
|
|
293
345
|
success: bool
|
|
294
|
-
|
|
346
|
+
deviceType: int
|
|
347
|
+
parallelMidboxSn: str | None = None
|
|
348
|
+
total: int
|
|
349
|
+
devices: list[ParallelGroupDeviceItem]
|
|
295
350
|
|
|
296
351
|
|
|
297
352
|
class InverterListResponse(BaseModel):
|
|
298
|
-
"""Inverter list API response."""
|
|
353
|
+
"""Inverter list API response (deprecated - use login response instead)."""
|
|
299
354
|
|
|
300
355
|
success: bool
|
|
301
356
|
rows: list[InverterDevice]
|
|
302
357
|
|
|
303
358
|
|
|
359
|
+
class InverterOverviewItem(BaseModel):
|
|
360
|
+
"""Inverter overview/status from inverterOverview/list endpoint."""
|
|
361
|
+
|
|
362
|
+
serialNum: str
|
|
363
|
+
statusText: str
|
|
364
|
+
deviceType: int
|
|
365
|
+
deviceTypeText: str
|
|
366
|
+
phase: int
|
|
367
|
+
plantId: int
|
|
368
|
+
plantName: str
|
|
369
|
+
ppv: int # PV power in watts
|
|
370
|
+
ppvText: str
|
|
371
|
+
pCharge: int # Charge power in watts
|
|
372
|
+
pChargeText: str
|
|
373
|
+
pDisCharge: int # Discharge power in watts
|
|
374
|
+
pDisChargeText: str
|
|
375
|
+
pConsumption: int # Consumption power in watts
|
|
376
|
+
pConsumptionText: str
|
|
377
|
+
soc: str # State of charge (e.g., "58 %")
|
|
378
|
+
vBat: int # Battery voltage (scaled: divide by 10 for actual volts)
|
|
379
|
+
vBatText: str
|
|
380
|
+
totalYielding: int # Total energy generated (raw: divide by 10 for kWh)
|
|
381
|
+
totalYieldingText: str
|
|
382
|
+
totalDischarging: int # Total energy discharged (raw: divide by 10 for kWh)
|
|
383
|
+
totalDischargingText: str
|
|
384
|
+
totalExport: int # Total energy exported (raw: divide by 10 for kWh)
|
|
385
|
+
totalExportText: str
|
|
386
|
+
totalUsage: int # Total energy consumed (raw: divide by 10 for kWh)
|
|
387
|
+
totalUsageText: str
|
|
388
|
+
parallelGroup: str
|
|
389
|
+
parallelIndex: str
|
|
390
|
+
parallelInfo: str
|
|
391
|
+
parallelModel: str
|
|
392
|
+
endUser: str | None = None # Account type: "guest", owner username, or installer name
|
|
393
|
+
|
|
394
|
+
@field_serializer("serialNum")
|
|
395
|
+
def serialize_serial(self, value: str) -> str:
|
|
396
|
+
"""Obfuscate serial number in serialized output."""
|
|
397
|
+
return _obfuscate_serial(value)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class InverterOverviewResponse(BaseModel):
|
|
401
|
+
"""Response from inverterOverview/list endpoint."""
|
|
402
|
+
|
|
403
|
+
success: bool
|
|
404
|
+
total: int
|
|
405
|
+
rows: list[InverterOverviewItem]
|
|
406
|
+
|
|
407
|
+
|
|
304
408
|
# Runtime Data Models
|
|
305
409
|
|
|
306
410
|
|
|
307
411
|
class InverterRuntime(BaseModel):
|
|
308
412
|
"""Inverter runtime data.
|
|
309
413
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
-
|
|
414
|
+
Raw values from API. Use property methods for scaled values.
|
|
415
|
+
|
|
416
|
+
Scaling applied by property methods:
|
|
417
|
+
- Most voltages: ÷10 (vpv1-3, vacr/s/t, vepsr/s/t, vBat)
|
|
418
|
+
- Bus voltages: ÷100 (vBus1, vBus2)
|
|
419
|
+
- Frequency: ÷100 (fac, feps, genFreq)
|
|
420
|
+
- Currents: ÷100 (maxChgCurr, maxDischgCurr)
|
|
314
421
|
- Power: no scaling (direct watts)
|
|
315
422
|
- Temperature: no scaling (direct Celsius)
|
|
423
|
+
|
|
424
|
+
See: constants.INVERTER_RUNTIME_SCALING for complete mapping
|
|
316
425
|
"""
|
|
317
426
|
|
|
318
427
|
success: bool
|
|
@@ -332,16 +441,20 @@ class InverterRuntime(BaseModel):
|
|
|
332
441
|
modelText: str | None = None
|
|
333
442
|
serverTime: str
|
|
334
443
|
deviceTime: str
|
|
444
|
+
deviceTimeText: str | None = None # Formatted device time (e.g., "2025/12/24")
|
|
335
445
|
# PV inputs (voltage requires �100)
|
|
336
446
|
vpv1: int
|
|
337
447
|
vpv2: int
|
|
338
448
|
vpv3: int | None = None
|
|
449
|
+
vpv4: int | None = None # Some models have 4 PV inputs
|
|
339
450
|
remainTime: int = 0
|
|
340
451
|
# PV power (watts, no scaling)
|
|
341
452
|
ppv1: int
|
|
342
453
|
ppv2: int
|
|
343
454
|
ppv3: int | None = None
|
|
455
|
+
ppv4: int | None = None # Some models have 4 PV inputs
|
|
344
456
|
ppv: int
|
|
457
|
+
ppvpCharge: int | None = None # PV charge power (alternate field name on some models)
|
|
345
458
|
# AC voltages (�100 for volts)
|
|
346
459
|
vacr: int
|
|
347
460
|
vacs: int
|
|
@@ -382,14 +495,17 @@ class InverterRuntime(BaseModel):
|
|
|
382
495
|
_12KAcCoupleInverterFlow: bool = False
|
|
383
496
|
_12KAcCoupleInverterData: bool = False
|
|
384
497
|
acCouplePower: int = 0
|
|
498
|
+
batteryCapacity: int | None = None # Battery capacity in Ah (int version of batCapacity)
|
|
385
499
|
# Other fields
|
|
386
500
|
hasEpsOverloadRecoveryTime: bool = False
|
|
387
|
-
|
|
388
|
-
|
|
501
|
+
# Note: These fields are optional as some models (e.g., 12000XP) don't return them
|
|
502
|
+
maxChgCurr: int = 0
|
|
503
|
+
maxDischgCurr: int = 0
|
|
389
504
|
maxChgCurrValue: int | None = None
|
|
390
505
|
maxDischgCurrValue: int | None = None
|
|
391
|
-
|
|
392
|
-
|
|
506
|
+
# BMS fields - optional as some models don't support BMS communication
|
|
507
|
+
bmsCharge: bool = False
|
|
508
|
+
bmsDischarge: bool = False
|
|
393
509
|
bmsForceCharge: bool = False
|
|
394
510
|
# Generator
|
|
395
511
|
_12KUsingGenerator: bool = False
|
|
@@ -403,8 +519,27 @@ class InverterRuntime(BaseModel):
|
|
|
403
519
|
pEpsL1N: int = 0
|
|
404
520
|
pEpsL2N: int = 0
|
|
405
521
|
haspEpsLNValue: bool = False
|
|
522
|
+
# Smart Load (12000XP and similar models)
|
|
523
|
+
smartLoadInverterFlow: bool = False
|
|
524
|
+
smartLoadInverterEnable: bool = False
|
|
525
|
+
epsLoadPowerShow: bool = False
|
|
526
|
+
gridLoadPowerShow: bool = False
|
|
527
|
+
pLoadPowerShow: bool = False
|
|
528
|
+
epsLoadPower: int = 0
|
|
529
|
+
gridLoadPower: int = 0
|
|
530
|
+
smartLoadPower: int = 0
|
|
406
531
|
# Directions
|
|
407
532
|
directions: dict[str, str] = Field(default_factory=dict)
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def pac(self) -> int:
|
|
536
|
+
"""AC output power (alias for pToUser for convenience).
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Power in watts flowing to user loads
|
|
540
|
+
"""
|
|
541
|
+
return self.pToUser
|
|
542
|
+
|
|
408
543
|
# Quick charge/discharge status
|
|
409
544
|
hasUnclosedQuickChargeTask: bool = False
|
|
410
545
|
hasUnclosedQuickDischargeTask: bool = False
|
|
@@ -416,7 +551,10 @@ class InverterRuntime(BaseModel):
|
|
|
416
551
|
class EnergyInfo(BaseModel):
|
|
417
552
|
"""Energy statistics data.
|
|
418
553
|
|
|
419
|
-
|
|
554
|
+
Raw energy values from API are in units of 0.1 kWh:
|
|
555
|
+
- Divide by 10 to get kWh directly
|
|
556
|
+
- Example: 184 → 18.4 kWh
|
|
557
|
+
|
|
420
558
|
Note: serialNum and soc are not present in parallel group energy responses.
|
|
421
559
|
"""
|
|
422
560
|
|
|
@@ -445,44 +583,182 @@ class EnergyInfo(BaseModel):
|
|
|
445
583
|
class BatteryModule(BaseModel):
|
|
446
584
|
"""Individual battery module information.
|
|
447
585
|
|
|
448
|
-
|
|
586
|
+
Raw values from API. Use Battery class properties for scaled values.
|
|
587
|
+
|
|
588
|
+
Scaling (applied by Battery class):
|
|
589
|
+
- totalVoltage: ÷100 (5305 → 53.05V)
|
|
590
|
+
- current: ÷10 (60 → 6.0A) **CRITICAL: Not ÷100**
|
|
591
|
+
- batMaxCellVoltage/batMinCellVoltage: ÷1000 (3317 → 3.317V)
|
|
592
|
+
- batMaxCellTemp/batMinCellTemp: ÷10 (240 → 24.0°C)
|
|
593
|
+
|
|
594
|
+
See: constants.BATTERY_MODULE_SCALING for complete mapping
|
|
449
595
|
"""
|
|
450
596
|
|
|
597
|
+
# Identification
|
|
451
598
|
batteryKey: str
|
|
452
599
|
batterySn: str
|
|
453
600
|
batIndex: int
|
|
601
|
+
batteryType: str | None = None
|
|
602
|
+
batteryTypeText: str | None = None
|
|
603
|
+
batBmsModelText: str | None = None
|
|
604
|
+
|
|
605
|
+
# Status
|
|
454
606
|
lost: bool
|
|
455
|
-
|
|
607
|
+
lastUpdateTime: str | None = None
|
|
608
|
+
|
|
609
|
+
# Voltage and Current (÷100 for volts, ÷10 for amps)
|
|
456
610
|
totalVoltage: int
|
|
457
|
-
# Current (�100 for amps)
|
|
458
611
|
current: int
|
|
612
|
+
|
|
613
|
+
# State of Charge/Health
|
|
459
614
|
soc: int
|
|
460
615
|
soh: int
|
|
616
|
+
|
|
617
|
+
# Capacity
|
|
461
618
|
currentRemainCapacity: int
|
|
462
619
|
currentFullCapacity: int
|
|
463
|
-
|
|
620
|
+
currentCapacityPercent: int | None = None
|
|
621
|
+
maxBatteryCharge: int | None = None
|
|
622
|
+
|
|
623
|
+
# Temperatures (÷10 for Celsius)
|
|
464
624
|
batMaxCellTemp: int
|
|
465
625
|
batMinCellTemp: int
|
|
466
|
-
|
|
626
|
+
batMaxCellNumTemp: int | None = None
|
|
627
|
+
batMinCellNumTemp: int | None = None
|
|
628
|
+
|
|
629
|
+
# Cell Voltages (÷1000 for volts)
|
|
467
630
|
batMaxCellVoltage: int
|
|
468
631
|
batMinCellVoltage: int
|
|
632
|
+
batMaxCellNumVolt: int | None = None
|
|
633
|
+
batMinCellNumVolt: int | None = None
|
|
634
|
+
|
|
635
|
+
# Charge Parameters
|
|
636
|
+
batChargeMaxCur: int | None = None
|
|
637
|
+
batChargeVoltRef: int | None = None
|
|
638
|
+
|
|
639
|
+
# Cycle Count and Firmware
|
|
469
640
|
cycleCnt: int
|
|
470
641
|
fwVersionText: str
|
|
471
642
|
|
|
643
|
+
# Additional Metrics (may be empty strings)
|
|
644
|
+
chgCapacity: str | None = None
|
|
645
|
+
disChgCapacity: str | None = None
|
|
646
|
+
ambientTemp: str | None = None
|
|
647
|
+
mosTemp: str | None = None
|
|
648
|
+
noticeInfo: str | None = None
|
|
649
|
+
|
|
472
650
|
|
|
473
651
|
class BatteryInfo(BaseModel):
|
|
474
|
-
"""Battery information including individual modules.
|
|
652
|
+
"""Battery information including individual modules.
|
|
653
|
+
|
|
654
|
+
This represents the aggregate battery system data from getBatteryInfo endpoint.
|
|
655
|
+
"""
|
|
475
656
|
|
|
476
657
|
success: bool
|
|
477
658
|
serialNum: str
|
|
659
|
+
|
|
660
|
+
# Status
|
|
661
|
+
lost: bool | None = None
|
|
662
|
+
hasRuntimeData: bool | None = None
|
|
663
|
+
statusText: str | None = None
|
|
664
|
+
batStatus: str
|
|
665
|
+
|
|
666
|
+
# State of Charge
|
|
478
667
|
soc: int
|
|
668
|
+
|
|
669
|
+
# Voltage (÷10 for volts at aggregate level)
|
|
479
670
|
vBat: int
|
|
671
|
+
totalVoltageText: str | None = None
|
|
672
|
+
|
|
673
|
+
# Power (direct watts)
|
|
674
|
+
ppv: int | None = None # PV power
|
|
480
675
|
pCharge: int
|
|
481
676
|
pDisCharge: int
|
|
482
|
-
|
|
677
|
+
batPower: int | None = None # Battery power
|
|
678
|
+
pinv: int | None = None # Inverter power
|
|
679
|
+
prec: int | None = None # Grid power
|
|
680
|
+
peps: int | None = None # EPS/backup power
|
|
681
|
+
|
|
682
|
+
# Capacity (Ah)
|
|
483
683
|
maxBatteryCharge: int
|
|
484
684
|
currentBatteryCharge: float
|
|
685
|
+
remainCapacity: int | None = None
|
|
686
|
+
fullCapacity: int | None = None
|
|
687
|
+
capacityPercent: int | None = None
|
|
688
|
+
|
|
689
|
+
# Current
|
|
690
|
+
currentText: str | None = None
|
|
691
|
+
currentType: str | None = None # "charge" or "discharge"
|
|
692
|
+
|
|
693
|
+
# Individual Battery Modules
|
|
485
694
|
batteryArray: list[BatteryModule]
|
|
695
|
+
totalNumber: int | None = None # Total battery count
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class BatteryListItem(BaseModel):
|
|
699
|
+
"""Simplified battery item from getBatteryInfoForSet endpoint."""
|
|
700
|
+
|
|
701
|
+
batteryKey: str
|
|
702
|
+
batterySn: str
|
|
703
|
+
batIndex: int
|
|
704
|
+
lost: bool
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
class BatteryListResponse(BaseModel):
|
|
708
|
+
"""Response from getBatteryInfoForSet endpoint.
|
|
709
|
+
|
|
710
|
+
This endpoint returns a simplified list of batteries without detailed metrics.
|
|
711
|
+
Use get_battery_info() for full battery metrics.
|
|
712
|
+
"""
|
|
713
|
+
|
|
714
|
+
success: bool
|
|
715
|
+
serialNum: str
|
|
716
|
+
totalNumber: int
|
|
717
|
+
batteryArray: list[BatteryListItem]
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class InverterDetail(BaseModel):
|
|
721
|
+
"""Inverter detail information."""
|
|
722
|
+
|
|
723
|
+
deviceText: str
|
|
724
|
+
fwCode: str
|
|
725
|
+
fwCodeText: str
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class InverterInfo(BaseModel):
|
|
729
|
+
"""Detailed inverter configuration and device information.
|
|
730
|
+
|
|
731
|
+
This endpoint returns static device configuration details,
|
|
732
|
+
not runtime data. Use get_inverter_runtime() for real-time metrics.
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
success: bool
|
|
736
|
+
lost: bool
|
|
737
|
+
datalogSn: str
|
|
738
|
+
serialNum: str
|
|
739
|
+
deviceType: int
|
|
740
|
+
phase: int
|
|
741
|
+
dtc: int
|
|
742
|
+
voltClass: int
|
|
743
|
+
fwVersion: int
|
|
744
|
+
hardwareVersion: int
|
|
745
|
+
subDeviceType: int
|
|
746
|
+
allowExport2Grid: bool
|
|
747
|
+
powerRating: int
|
|
748
|
+
machineType: int
|
|
749
|
+
deviceTypeText: str
|
|
750
|
+
inverterDetail: InverterDetail
|
|
751
|
+
deviceInfo: str
|
|
752
|
+
address: int
|
|
753
|
+
powerRatingText: str
|
|
754
|
+
batteryType: BatteryType
|
|
755
|
+
status: int
|
|
756
|
+
statusText: str
|
|
757
|
+
|
|
758
|
+
@field_serializer("serialNum", "datalogSn")
|
|
759
|
+
def serialize_serial(self, value: str) -> str:
|
|
760
|
+
"""Obfuscate serial numbers in serialized output."""
|
|
761
|
+
return _obfuscate_serial(value)
|
|
486
762
|
|
|
487
763
|
|
|
488
764
|
# GridBOSS/MID Device Models
|
|
@@ -491,24 +767,41 @@ class BatteryInfo(BaseModel):
|
|
|
491
767
|
class MidboxData(BaseModel):
|
|
492
768
|
"""GridBOSS/MID device runtime data.
|
|
493
769
|
|
|
494
|
-
Note: Voltages, currents
|
|
770
|
+
Note: Voltages are in decivolts (÷10), currents in centiamps (÷100),
|
|
771
|
+
frequency in centihertz (÷100). Power values are in watts (no scaling).
|
|
495
772
|
"""
|
|
496
773
|
|
|
497
774
|
status: int
|
|
498
775
|
serverTime: str
|
|
499
776
|
deviceTime: str
|
|
500
|
-
# Grid voltages (�
|
|
777
|
+
# Grid voltages (�10 for volts, e.g., 2418 = 241.8V)
|
|
501
778
|
gridRmsVolt: int
|
|
502
779
|
upsRmsVolt: int
|
|
503
780
|
genRmsVolt: int
|
|
504
781
|
gridL1RmsVolt: int
|
|
505
782
|
gridL2RmsVolt: int
|
|
506
|
-
|
|
783
|
+
upsL1RmsVolt: int
|
|
784
|
+
upsL2RmsVolt: int
|
|
785
|
+
genL1RmsVolt: int
|
|
786
|
+
genL2RmsVolt: int
|
|
787
|
+
# Currents (�100 for amps)
|
|
507
788
|
gridL1RmsCurr: int
|
|
508
789
|
gridL2RmsCurr: int
|
|
790
|
+
loadL1RmsCurr: int
|
|
791
|
+
loadL2RmsCurr: int
|
|
792
|
+
genL1RmsCurr: int
|
|
793
|
+
genL2RmsCurr: int
|
|
794
|
+
upsL1RmsCurr: int
|
|
795
|
+
upsL2RmsCurr: int
|
|
509
796
|
# Power (watts, no scaling)
|
|
510
797
|
gridL1ActivePower: int
|
|
511
798
|
gridL2ActivePower: int
|
|
799
|
+
loadL1ActivePower: int
|
|
800
|
+
loadL2ActivePower: int
|
|
801
|
+
genL1ActivePower: int
|
|
802
|
+
genL2ActivePower: int
|
|
803
|
+
upsL1ActivePower: int
|
|
804
|
+
upsL2ActivePower: int
|
|
512
805
|
hybridPower: int
|
|
513
806
|
# Smart port status
|
|
514
807
|
smartPort1Status: int
|
|
@@ -570,10 +863,15 @@ class ParameterReadResponse(BaseModel):
|
|
|
570
863
|
|
|
571
864
|
|
|
572
865
|
class QuickChargeStatus(BaseModel):
|
|
573
|
-
"""Quick charge status response.
|
|
866
|
+
"""Quick charge/discharge status response.
|
|
867
|
+
|
|
868
|
+
Note: The quickCharge/getStatusInfo endpoint returns status for BOTH
|
|
869
|
+
quick charge and quick discharge operations.
|
|
870
|
+
"""
|
|
574
871
|
|
|
575
872
|
success: bool
|
|
576
873
|
hasUnclosedQuickChargeTask: bool
|
|
874
|
+
hasUnclosedQuickDischargeTask: bool = False # May not be present in older API versions
|
|
577
875
|
|
|
578
876
|
|
|
579
877
|
class SuccessResponse(BaseModel):
|
|
@@ -683,17 +981,20 @@ class FirmwareUpdateDetails(BaseModel):
|
|
|
683
981
|
"""Obfuscate serial number in serialized output."""
|
|
684
982
|
return _obfuscate_serial(value)
|
|
685
983
|
|
|
984
|
+
@property
|
|
686
985
|
def has_app_update(self) -> bool:
|
|
687
986
|
"""Check if application firmware update is available."""
|
|
688
987
|
return self.lastV1 is not None and self.v1 < self.lastV1 and self.pcs1UpdateMatch
|
|
689
988
|
|
|
989
|
+
@property
|
|
690
990
|
def has_parameter_update(self) -> bool:
|
|
691
991
|
"""Check if parameter firmware update is available."""
|
|
692
992
|
return self.lastV2 is not None and self.v2 < self.lastV2 and self.pcs2UpdateMatch
|
|
693
993
|
|
|
994
|
+
@property
|
|
694
995
|
def has_update(self) -> bool:
|
|
695
996
|
"""Check if any firmware update is available."""
|
|
696
|
-
return self.has_app_update
|
|
997
|
+
return self.has_app_update or self.has_parameter_update
|
|
697
998
|
|
|
698
999
|
|
|
699
1000
|
class FirmwareUpdateCheck(BaseModel):
|
|
@@ -703,6 +1004,48 @@ class FirmwareUpdateCheck(BaseModel):
|
|
|
703
1004
|
details: FirmwareUpdateDetails
|
|
704
1005
|
infoForwardUrl: str | None = None
|
|
705
1006
|
|
|
1007
|
+
@classmethod
|
|
1008
|
+
def create_up_to_date(cls, serial_num: str) -> FirmwareUpdateCheck:
|
|
1009
|
+
"""Create a FirmwareUpdateCheck indicating firmware is already up to date.
|
|
1010
|
+
|
|
1011
|
+
This is used when the API returns a "firmware is already the latest version"
|
|
1012
|
+
message, which should be treated as a successful check with no update available.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
serial_num: Device serial number
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
FirmwareUpdateCheck with details indicating no update is available
|
|
1019
|
+
"""
|
|
1020
|
+
# Create minimal details with no update available
|
|
1021
|
+
# All version fields set to 0, no latest versions, compatibility flags False
|
|
1022
|
+
details = FirmwareUpdateDetails(
|
|
1023
|
+
serialNum=serial_num,
|
|
1024
|
+
deviceType=0,
|
|
1025
|
+
standard="",
|
|
1026
|
+
firmwareType="",
|
|
1027
|
+
fwCodeBeforeUpload="",
|
|
1028
|
+
v1=0, # Current version unknown
|
|
1029
|
+
v2=0,
|
|
1030
|
+
v3Value=0,
|
|
1031
|
+
lastV1=None, # No update available
|
|
1032
|
+
lastV1FileName=None,
|
|
1033
|
+
lastV2=None,
|
|
1034
|
+
lastV2FileName=None,
|
|
1035
|
+
m3Version=0,
|
|
1036
|
+
pcs1UpdateMatch=False, # No update match
|
|
1037
|
+
pcs2UpdateMatch=False,
|
|
1038
|
+
pcs3UpdateMatch=False,
|
|
1039
|
+
needRunStep2=False,
|
|
1040
|
+
needRunStep3=False,
|
|
1041
|
+
needRunStep4=False,
|
|
1042
|
+
needRunStep5=False,
|
|
1043
|
+
midbox=False,
|
|
1044
|
+
lowVoltBattery=False,
|
|
1045
|
+
type6=False,
|
|
1046
|
+
)
|
|
1047
|
+
return cls(success=True, details=details, infoForwardUrl=None)
|
|
1048
|
+
|
|
706
1049
|
|
|
707
1050
|
class FirmwareDeviceInfo(BaseModel):
|
|
708
1051
|
"""Individual device firmware update information."""
|
|
@@ -724,20 +1067,55 @@ class FirmwareDeviceInfo(BaseModel):
|
|
|
724
1067
|
"""Obfuscate serial number in serialized output."""
|
|
725
1068
|
return _obfuscate_serial(value)
|
|
726
1069
|
|
|
1070
|
+
@property
|
|
727
1071
|
def is_in_progress(self) -> bool:
|
|
728
|
-
"""Check if update is currently in progress.
|
|
1072
|
+
"""Check if update is currently in progress.
|
|
1073
|
+
|
|
1074
|
+
Uses multiple indicators for reliable detection:
|
|
1075
|
+
- updateStatus must be UPLOADING or READY
|
|
1076
|
+
- isSendEndUpdate must be False (not completed yet)
|
|
1077
|
+
- isSendStartUpdate should be True (update has started)
|
|
1078
|
+
|
|
1079
|
+
This ensures we accurately detect active updates and avoid
|
|
1080
|
+
false positives from completed or failed updates.
|
|
1081
|
+
|
|
1082
|
+
Returns:
|
|
1083
|
+
True if update is actively in progress, False otherwise
|
|
1084
|
+
"""
|
|
729
1085
|
return (
|
|
730
|
-
self.updateStatus == UpdateStatus.UPLOADING or self.updateStatus == UpdateStatus.READY
|
|
1086
|
+
(self.updateStatus == UpdateStatus.UPLOADING or self.updateStatus == UpdateStatus.READY)
|
|
1087
|
+
and not self.isSendEndUpdate
|
|
1088
|
+
and self.isSendStartUpdate
|
|
731
1089
|
)
|
|
732
1090
|
|
|
1091
|
+
@property
|
|
733
1092
|
def is_complete(self) -> bool:
|
|
734
|
-
"""Check if update completed successfully.
|
|
1093
|
+
"""Check if update completed successfully.
|
|
1094
|
+
|
|
1095
|
+
Uses multiple indicators for reliable detection:
|
|
1096
|
+
- updateStatus is SUCCESS or COMPLETE
|
|
1097
|
+
- isSendEndUpdate is True (end notification sent)
|
|
1098
|
+
- stopTime is populated (not empty string)
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
True if update completed successfully, False otherwise
|
|
1102
|
+
"""
|
|
735
1103
|
return (
|
|
736
|
-
|
|
1104
|
+
(
|
|
1105
|
+
self.updateStatus == UpdateStatus.SUCCESS
|
|
1106
|
+
or self.updateStatus == UpdateStatus.COMPLETE
|
|
1107
|
+
)
|
|
1108
|
+
and self.isSendEndUpdate
|
|
1109
|
+
and bool(self.stopTime.strip())
|
|
737
1110
|
)
|
|
738
1111
|
|
|
1112
|
+
@property
|
|
739
1113
|
def is_failed(self) -> bool:
|
|
740
|
-
"""Check if update failed.
|
|
1114
|
+
"""Check if update failed.
|
|
1115
|
+
|
|
1116
|
+
Returns:
|
|
1117
|
+
True if update failed, False otherwise
|
|
1118
|
+
"""
|
|
741
1119
|
return self.updateStatus == UpdateStatus.FAILED
|
|
742
1120
|
|
|
743
1121
|
|
|
@@ -749,9 +1127,10 @@ class FirmwareUpdateStatus(BaseModel):
|
|
|
749
1127
|
fileReady: bool
|
|
750
1128
|
deviceInfos: list[FirmwareDeviceInfo]
|
|
751
1129
|
|
|
1130
|
+
@property
|
|
752
1131
|
def has_active_updates(self) -> bool:
|
|
753
1132
|
"""Check if any device has an active update."""
|
|
754
|
-
return any(device.is_in_progress
|
|
1133
|
+
return any(device.is_in_progress for device in self.deviceInfos)
|
|
755
1134
|
|
|
756
1135
|
|
|
757
1136
|
class UpdateEligibilityStatus(BaseModel):
|
|
@@ -760,6 +1139,216 @@ class UpdateEligibilityStatus(BaseModel):
|
|
|
760
1139
|
success: bool
|
|
761
1140
|
msg: UpdateEligibilityMessage
|
|
762
1141
|
|
|
1142
|
+
@property
|
|
763
1143
|
def is_allowed(self) -> bool:
|
|
764
1144
|
"""Check if device is allowed to update."""
|
|
765
1145
|
return self.msg == UpdateEligibilityMessage.ALLOW_TO_UPDATE
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
class FirmwareUpdateInfo(BaseModel):
|
|
1149
|
+
"""Home Assistant-friendly firmware update information.
|
|
1150
|
+
|
|
1151
|
+
This model provides all fields needed to create an Update entity in Home Assistant,
|
|
1152
|
+
including required properties (installed_version, latest_version, title) and
|
|
1153
|
+
optional properties (release_summary, release_url, in_progress, etc.).
|
|
1154
|
+
|
|
1155
|
+
Example:
|
|
1156
|
+
```python
|
|
1157
|
+
update_info = await inverter.get_firmware_update_info()
|
|
1158
|
+
if update_info.update_available:
|
|
1159
|
+
print(f"Update: {update_info.installed_version} → {update_info.latest_version}")
|
|
1160
|
+
print(f"Release notes: {update_info.release_url}")
|
|
1161
|
+
print(f"Summary: {update_info.release_summary}")
|
|
1162
|
+
```
|
|
1163
|
+
"""
|
|
1164
|
+
|
|
1165
|
+
# Required HA Update Entity properties
|
|
1166
|
+
installed_version: str # Current firmware version (e.g., "IAAB-1300")
|
|
1167
|
+
latest_version: str # Latest available version
|
|
1168
|
+
title: str # Software title (e.g., "Inverter Firmware", "GridBOSS Firmware")
|
|
1169
|
+
|
|
1170
|
+
# Optional HA Update Entity properties
|
|
1171
|
+
release_summary: str | None = None # Brief changelog (max 255 chars)
|
|
1172
|
+
release_url: str | None = None # URL to full release notes
|
|
1173
|
+
in_progress: bool = False # Whether update is currently installing
|
|
1174
|
+
update_percentage: int | None = None # Installation progress (0-100)
|
|
1175
|
+
|
|
1176
|
+
# Additional metadata for HA entity configuration
|
|
1177
|
+
device_class: str = "firmware" # UpdateDeviceClass.FIRMWARE
|
|
1178
|
+
supported_features: list[str] = [] # e.g., ["install", "progress", "release_notes"]
|
|
1179
|
+
|
|
1180
|
+
# Raw API data for advanced use
|
|
1181
|
+
app_version_current: int | None = None # v1 from API
|
|
1182
|
+
app_version_latest: int | None = None # lastV1 from API
|
|
1183
|
+
param_version_current: int | None = None # v2 from API
|
|
1184
|
+
param_version_latest: int | None = None # lastV2 from API
|
|
1185
|
+
app_filename: str | None = None # lastV1FileName from API
|
|
1186
|
+
param_filename: str | None = None # lastV2FileName from API
|
|
1187
|
+
|
|
1188
|
+
@property
|
|
1189
|
+
def update_available(self) -> bool:
|
|
1190
|
+
"""Check if firmware update is available.
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
True if latest_version is newer than installed_version.
|
|
1194
|
+
"""
|
|
1195
|
+
return self.installed_version != self.latest_version
|
|
1196
|
+
|
|
1197
|
+
@property
|
|
1198
|
+
def has_app_update(self) -> bool:
|
|
1199
|
+
"""Check if application firmware update is available.
|
|
1200
|
+
|
|
1201
|
+
Returns:
|
|
1202
|
+
True if app firmware update is available.
|
|
1203
|
+
"""
|
|
1204
|
+
return (
|
|
1205
|
+
self.app_version_current is not None
|
|
1206
|
+
and self.app_version_latest is not None
|
|
1207
|
+
and self.app_version_current < self.app_version_latest
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
@property
|
|
1211
|
+
def has_parameter_update(self) -> bool:
|
|
1212
|
+
"""Check if parameter firmware update is available.
|
|
1213
|
+
|
|
1214
|
+
Returns:
|
|
1215
|
+
True if parameter firmware update is available.
|
|
1216
|
+
"""
|
|
1217
|
+
return (
|
|
1218
|
+
self.param_version_current is not None
|
|
1219
|
+
and self.param_version_latest is not None
|
|
1220
|
+
and self.param_version_current < self.param_version_latest
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
@classmethod
|
|
1224
|
+
def from_api_response(
|
|
1225
|
+
cls,
|
|
1226
|
+
check: FirmwareUpdateCheck,
|
|
1227
|
+
title: str,
|
|
1228
|
+
in_progress: bool = False,
|
|
1229
|
+
update_percentage: int | None = None,
|
|
1230
|
+
) -> FirmwareUpdateInfo:
|
|
1231
|
+
"""Create FirmwareUpdateInfo from API response.
|
|
1232
|
+
|
|
1233
|
+
Args:
|
|
1234
|
+
check: FirmwareUpdateCheck from API
|
|
1235
|
+
title: Device title (e.g., "FlexBOSS21 Firmware", "GridBOSS Firmware")
|
|
1236
|
+
in_progress: Whether update is currently installing
|
|
1237
|
+
update_percentage: Installation progress (0-100)
|
|
1238
|
+
|
|
1239
|
+
Returns:
|
|
1240
|
+
FirmwareUpdateInfo instance with HA-compatible fields.
|
|
1241
|
+
|
|
1242
|
+
Example:
|
|
1243
|
+
```python
|
|
1244
|
+
api_check = await client.firmware.check_firmware_updates(serial)
|
|
1245
|
+
update_info = FirmwareUpdateInfo.from_api_response(
|
|
1246
|
+
api_check,
|
|
1247
|
+
title="FlexBOSS21 Firmware"
|
|
1248
|
+
)
|
|
1249
|
+
```
|
|
1250
|
+
"""
|
|
1251
|
+
details = check.details
|
|
1252
|
+
|
|
1253
|
+
# Construct latest version from lastV1/lastV2 (or use current if no updates)
|
|
1254
|
+
# Format: {fwCode}-{v1_hex}{v2_hex} (e.g., "IAAB-1600" for v1=22, v2=0)
|
|
1255
|
+
# Note: API returns decimal values, but firmware versions use hexadecimal
|
|
1256
|
+
if details.has_app_update or details.has_parameter_update:
|
|
1257
|
+
# Use lastV1/lastV2 if there's an actual update, otherwise use current
|
|
1258
|
+
latest_v1 = details.lastV1 if details.has_app_update else details.v1
|
|
1259
|
+
latest_v2 = details.lastV2 if details.has_parameter_update else details.v2
|
|
1260
|
+
# Extract firmware code (e.g., "IAAB" from "IAAB-1300")
|
|
1261
|
+
fw_code = (
|
|
1262
|
+
details.fwCodeBeforeUpload.split("-")[0]
|
|
1263
|
+
if "-" in details.fwCodeBeforeUpload
|
|
1264
|
+
else details.fwCodeBeforeUpload[:4]
|
|
1265
|
+
)
|
|
1266
|
+
# Convert to 2-digit hex (uppercase to match API format)
|
|
1267
|
+
latest_version = f"{fw_code}-{latest_v1:02X}{latest_v2:02X}"
|
|
1268
|
+
else:
|
|
1269
|
+
# No updates available
|
|
1270
|
+
latest_version = details.fwCodeBeforeUpload
|
|
1271
|
+
|
|
1272
|
+
# Generate release summary (max 255 chars for HA)
|
|
1273
|
+
# Format as hex to match firmware version format (e.g., IAAB-1300 means v1=0x13)
|
|
1274
|
+
summary_parts = []
|
|
1275
|
+
if details.has_app_update:
|
|
1276
|
+
summary_parts.append(f"App firmware: v{details.v1:02X} → v{details.lastV1:02X}")
|
|
1277
|
+
if details.has_parameter_update:
|
|
1278
|
+
summary_parts.append(f"Parameter firmware: v{details.v2:02X} → v{details.lastV2:02X}")
|
|
1279
|
+
release_summary = "; ".join(summary_parts) if summary_parts else None
|
|
1280
|
+
|
|
1281
|
+
# Determine supported features based on API capabilities
|
|
1282
|
+
supported_features = ["install"] # All devices support install
|
|
1283
|
+
if update_percentage is not None:
|
|
1284
|
+
supported_features.append("progress")
|
|
1285
|
+
if check.infoForwardUrl:
|
|
1286
|
+
supported_features.append("release_notes")
|
|
1287
|
+
|
|
1288
|
+
return cls(
|
|
1289
|
+
# Required HA properties
|
|
1290
|
+
installed_version=details.fwCodeBeforeUpload,
|
|
1291
|
+
latest_version=latest_version,
|
|
1292
|
+
title=title,
|
|
1293
|
+
# Optional HA properties
|
|
1294
|
+
release_summary=release_summary,
|
|
1295
|
+
release_url=check.infoForwardUrl,
|
|
1296
|
+
in_progress=in_progress,
|
|
1297
|
+
update_percentage=update_percentage,
|
|
1298
|
+
# Metadata
|
|
1299
|
+
device_class="firmware",
|
|
1300
|
+
supported_features=supported_features,
|
|
1301
|
+
# Raw API data
|
|
1302
|
+
app_version_current=details.v1,
|
|
1303
|
+
app_version_latest=details.lastV1,
|
|
1304
|
+
param_version_current=details.v2,
|
|
1305
|
+
param_version_latest=details.lastV2,
|
|
1306
|
+
app_filename=details.lastV1FileName,
|
|
1307
|
+
param_filename=details.lastV2FileName,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
# Dongle Connection Status Models
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
class DongleStatus(BaseModel):
|
|
1315
|
+
"""Dongle connection status from findOnlineDatalog endpoint.
|
|
1316
|
+
|
|
1317
|
+
The dongle (datalog) is the communication module that connects
|
|
1318
|
+
inverters to the cloud monitoring service. This model represents
|
|
1319
|
+
its current online/offline status.
|
|
1320
|
+
|
|
1321
|
+
The API returns:
|
|
1322
|
+
- msg: "current" when dongle is actively communicating
|
|
1323
|
+
- msg: "" (empty) when dongle is offline/not communicating
|
|
1324
|
+
|
|
1325
|
+
Example:
|
|
1326
|
+
```python
|
|
1327
|
+
status = await client.devices.get_dongle_status("BC34000380")
|
|
1328
|
+
if status.is_online:
|
|
1329
|
+
print("Dongle is online and communicating")
|
|
1330
|
+
else:
|
|
1331
|
+
print("Dongle is offline - inverter data may be stale")
|
|
1332
|
+
```
|
|
1333
|
+
"""
|
|
1334
|
+
|
|
1335
|
+
success: bool
|
|
1336
|
+
msg: str = ""
|
|
1337
|
+
|
|
1338
|
+
@property
|
|
1339
|
+
def is_online(self) -> bool:
|
|
1340
|
+
"""Check if the dongle is currently online.
|
|
1341
|
+
|
|
1342
|
+
Returns:
|
|
1343
|
+
True if dongle is actively communicating, False otherwise.
|
|
1344
|
+
"""
|
|
1345
|
+
return self.msg == "current"
|
|
1346
|
+
|
|
1347
|
+
@property
|
|
1348
|
+
def status_text(self) -> str:
|
|
1349
|
+
"""Get human-readable status text.
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
"Online" or "Offline" based on dongle status.
|
|
1353
|
+
"""
|
|
1354
|
+
return "Online" if self.is_online else "Offline"
|