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,504 @@
|
|
|
1
|
+
"""Firmware update detection mixin for devices.
|
|
2
|
+
|
|
3
|
+
This module provides the FirmwareUpdateMixin class that can be mixed into
|
|
4
|
+
any device class (BaseInverter, MIDDevice, etc.) to add firmware update
|
|
5
|
+
detection capabilities with caching and Home Assistant compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pylxpweb import LuxpowerClient
|
|
16
|
+
from pylxpweb.models import FirmwareUpdateInfo
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FirmwareUpdateMixin:
|
|
20
|
+
"""Mixin class providing firmware update detection for devices.
|
|
21
|
+
|
|
22
|
+
This mixin adds:
|
|
23
|
+
- Firmware update checking with 24-hour caching
|
|
24
|
+
- Real-time progress tracking with adaptive caching
|
|
25
|
+
- Synchronous property access to cached update status
|
|
26
|
+
- Methods to start updates and check eligibility
|
|
27
|
+
- Full Home Assistant Update entity compatibility
|
|
28
|
+
|
|
29
|
+
Available properties (synchronous, cached):
|
|
30
|
+
- firmware_update_available: bool | None - Update availability
|
|
31
|
+
- firmware_update_in_progress: bool - Update currently in progress
|
|
32
|
+
- firmware_update_percentage: int | None - Progress percentage (0-100)
|
|
33
|
+
- latest_firmware_version: str | None - Latest version available
|
|
34
|
+
- firmware_update_title: str | None - Update title
|
|
35
|
+
- firmware_update_summary: str | None - Release summary
|
|
36
|
+
- firmware_update_url: str | None - Release notes URL
|
|
37
|
+
|
|
38
|
+
The mixin expects the following attributes on the implementing class:
|
|
39
|
+
- _client: LuxpowerClient instance
|
|
40
|
+
- serial_number: Device serial number (str)
|
|
41
|
+
- model: Device model name (str)
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
class MyDevice(FirmwareUpdateMixin, BaseDevice):
|
|
46
|
+
def __init__(self, client, serial_number, model):
|
|
47
|
+
super().__init__(client, serial_number, model)
|
|
48
|
+
self._init_firmware_update_cache()
|
|
49
|
+
|
|
50
|
+
# ... rest of device implementation
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def _init_firmware_update_cache(self) -> None:
|
|
55
|
+
"""Initialize firmware update cache attributes.
|
|
56
|
+
|
|
57
|
+
This method must be called in the device's __init__ after super().__init__().
|
|
58
|
+
It initializes the cache attributes needed for firmware update detection.
|
|
59
|
+
"""
|
|
60
|
+
self._firmware_update_info: FirmwareUpdateInfo | None = None
|
|
61
|
+
self._firmware_update_cache_time: datetime | None = None
|
|
62
|
+
self._firmware_update_cache_ttl = timedelta(hours=24) # 24-hour TTL
|
|
63
|
+
self._firmware_update_cache_lock = asyncio.Lock()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def firmware_update_available(self) -> bool | None:
|
|
67
|
+
"""Check if firmware update is available (from cache).
|
|
68
|
+
|
|
69
|
+
This property provides synchronous access to cached firmware update status.
|
|
70
|
+
Returns None if firmware check has never been performed.
|
|
71
|
+
|
|
72
|
+
To check for updates, call `check_firmware_updates()` first.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if update available, False if up to date, None if not checked yet.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> # First check for updates
|
|
79
|
+
>>> update_info = await device.check_firmware_updates()
|
|
80
|
+
>>> # Then access cached status
|
|
81
|
+
>>> if device.firmware_update_available:
|
|
82
|
+
... print(f"Update available: {update_info.release_summary}")
|
|
83
|
+
"""
|
|
84
|
+
if self._firmware_update_info is None:
|
|
85
|
+
return None
|
|
86
|
+
return self._firmware_update_info.update_available
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def latest_firmware_version(self) -> str | None:
|
|
90
|
+
"""Get latest firmware version from cache.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Latest firmware version string, or None if not checked yet.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> await device.check_firmware_updates()
|
|
97
|
+
>>> print(f"Latest version: {device.latest_firmware_version}")
|
|
98
|
+
"""
|
|
99
|
+
if self._firmware_update_info is None:
|
|
100
|
+
return None
|
|
101
|
+
return self._firmware_update_info.latest_version
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def firmware_update_title(self) -> str | None:
|
|
105
|
+
"""Get firmware update title from cache.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Firmware update title, or None if not checked yet.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> await device.check_firmware_updates()
|
|
112
|
+
>>> print(f"Title: {device.firmware_update_title}")
|
|
113
|
+
"""
|
|
114
|
+
if self._firmware_update_info is None:
|
|
115
|
+
return None
|
|
116
|
+
return self._firmware_update_info.title
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def firmware_update_summary(self) -> str | None:
|
|
120
|
+
"""Get firmware update summary from cache.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Firmware update release summary, or None if not checked yet.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> await device.check_firmware_updates()
|
|
127
|
+
>>> if device.firmware_update_summary:
|
|
128
|
+
... print(f"Summary: {device.firmware_update_summary}")
|
|
129
|
+
"""
|
|
130
|
+
if self._firmware_update_info is None:
|
|
131
|
+
return None
|
|
132
|
+
return self._firmware_update_info.release_summary
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def firmware_update_url(self) -> str | None:
|
|
136
|
+
"""Get firmware update URL from cache.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Firmware update release URL, or None if not checked yet.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> await device.check_firmware_updates()
|
|
143
|
+
>>> if device.firmware_update_url:
|
|
144
|
+
... print(f"Release notes: {device.firmware_update_url}")
|
|
145
|
+
"""
|
|
146
|
+
if self._firmware_update_info is None:
|
|
147
|
+
return None
|
|
148
|
+
return self._firmware_update_info.release_url
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def firmware_update_in_progress(self) -> bool:
|
|
152
|
+
"""Check if firmware update is currently in progress (from cache).
|
|
153
|
+
|
|
154
|
+
This property provides synchronous access to cached firmware update progress status.
|
|
155
|
+
Returns False if no progress data available or if no update is in progress.
|
|
156
|
+
|
|
157
|
+
To get real-time progress, call `get_firmware_update_progress()` first.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if update is in progress, False otherwise.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> # Check progress
|
|
164
|
+
>>> await device.get_firmware_update_progress()
|
|
165
|
+
>>> # Access cached status
|
|
166
|
+
>>> if device.firmware_update_in_progress:
|
|
167
|
+
... print(f"Update at {device.firmware_update_percentage}%")
|
|
168
|
+
"""
|
|
169
|
+
if self._firmware_update_info is None:
|
|
170
|
+
return False
|
|
171
|
+
return self._firmware_update_info.in_progress
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def firmware_update_percentage(self) -> int | None:
|
|
175
|
+
"""Get firmware update progress percentage (from cache).
|
|
176
|
+
|
|
177
|
+
This property provides synchronous access to cached firmware update progress percentage.
|
|
178
|
+
Returns None if no progress data available.
|
|
179
|
+
|
|
180
|
+
To get real-time progress, call `get_firmware_update_progress()` first.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Progress percentage (0-100), or None if not available.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> # Check progress
|
|
187
|
+
>>> await device.get_firmware_update_progress()
|
|
188
|
+
>>> # Access cached percentage
|
|
189
|
+
>>> if device.firmware_update_percentage is not None:
|
|
190
|
+
... print(f"Progress: {device.firmware_update_percentage}%")
|
|
191
|
+
"""
|
|
192
|
+
if self._firmware_update_info is None:
|
|
193
|
+
return None
|
|
194
|
+
return self._firmware_update_info.update_percentage
|
|
195
|
+
|
|
196
|
+
async def check_firmware_updates(self, force: bool = False) -> FirmwareUpdateInfo:
|
|
197
|
+
"""Check for available firmware updates (cached with 24-hour TTL).
|
|
198
|
+
|
|
199
|
+
This method checks the API for firmware updates and caches the result
|
|
200
|
+
for 24 hours. Subsequent calls within the cache period will return
|
|
201
|
+
cached data unless force=True.
|
|
202
|
+
|
|
203
|
+
The returned FirmwareUpdateInfo contains all fields needed for Home
|
|
204
|
+
Assistant Update entities, including installed_version, latest_version,
|
|
205
|
+
release_summary, release_url, and supported_features.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
force: If True, bypass cache and force fresh check from API
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
FirmwareUpdateInfo instance with HA-compatible update information.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
LuxpowerAPIError: If API check fails
|
|
215
|
+
LuxpowerConnectionError: If network connection fails
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
>>> # Check for updates (cached for 24 hours)
|
|
219
|
+
>>> update_info = await device.check_firmware_updates()
|
|
220
|
+
>>> if update_info.update_available:
|
|
221
|
+
... print(f"New version: {update_info.latest_version}")
|
|
222
|
+
... print(f"Summary: {update_info.release_summary}")
|
|
223
|
+
... print(f"Release notes: {update_info.release_url}")
|
|
224
|
+
...
|
|
225
|
+
>>> # Access cached status synchronously
|
|
226
|
+
>>> if device.firmware_update_available:
|
|
227
|
+
... print("Update available!")
|
|
228
|
+
"""
|
|
229
|
+
# Import here to avoid circular imports
|
|
230
|
+
from pylxpweb.models import FirmwareUpdateInfo
|
|
231
|
+
|
|
232
|
+
# Check cache
|
|
233
|
+
if not force:
|
|
234
|
+
async with self._firmware_update_cache_lock:
|
|
235
|
+
if (
|
|
236
|
+
self._firmware_update_cache_time is not None
|
|
237
|
+
and (datetime.now() - self._firmware_update_cache_time)
|
|
238
|
+
< self._firmware_update_cache_ttl
|
|
239
|
+
):
|
|
240
|
+
assert self._firmware_update_info is not None
|
|
241
|
+
return self._firmware_update_info
|
|
242
|
+
|
|
243
|
+
# Fetch from API
|
|
244
|
+
client: LuxpowerClient = self._client # type: ignore[attr-defined]
|
|
245
|
+
serial: str = self.serial_number # type: ignore[attr-defined]
|
|
246
|
+
model: str = self.model # type: ignore[attr-defined]
|
|
247
|
+
|
|
248
|
+
check = await client.api.firmware.check_firmware_updates(serial)
|
|
249
|
+
|
|
250
|
+
# Create HA-friendly update info
|
|
251
|
+
title = f"{model} Firmware"
|
|
252
|
+
update_info = FirmwareUpdateInfo.from_api_response(check, title=title)
|
|
253
|
+
|
|
254
|
+
# Update cache
|
|
255
|
+
async with self._firmware_update_cache_lock:
|
|
256
|
+
self._firmware_update_info = update_info
|
|
257
|
+
self._firmware_update_cache_time = datetime.now()
|
|
258
|
+
|
|
259
|
+
return update_info
|
|
260
|
+
|
|
261
|
+
async def get_firmware_update_progress(self, force: bool = False) -> FirmwareUpdateInfo:
|
|
262
|
+
"""Get real-time firmware update progress for this device.
|
|
263
|
+
|
|
264
|
+
This method queries the API for current firmware update status and returns
|
|
265
|
+
updated FirmwareUpdateInfo with real-time progress data.
|
|
266
|
+
|
|
267
|
+
Caching behavior (adaptive based on update status):
|
|
268
|
+
- During active updates (in_progress=True): 10-second cache for near real-time progress
|
|
269
|
+
- No active update (in_progress=False): 5-minute cache to reduce API load
|
|
270
|
+
- force=True: Always bypasses cache regardless of status
|
|
271
|
+
|
|
272
|
+
The short 10-second cache during updates provides fresh progress data while
|
|
273
|
+
preventing excessive API calls if multiple components poll simultaneously.
|
|
274
|
+
|
|
275
|
+
Use this method when:
|
|
276
|
+
- Monitoring active firmware update progress
|
|
277
|
+
- Checking if update is in progress
|
|
278
|
+
- Getting current update percentage during installation
|
|
279
|
+
|
|
280
|
+
The returned FirmwareUpdateInfo will have:
|
|
281
|
+
- in_progress: True if update is currently active (UPLOADING/READY)
|
|
282
|
+
- update_percentage: Current progress (0-100) parsed from API
|
|
283
|
+
- All other fields from cached firmware check
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
force: If True, bypass cache and force fresh check from API
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
FirmwareUpdateInfo with real-time progress data
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
LuxpowerAPIError: If API check fails
|
|
293
|
+
LuxpowerConnectionError: If network connection fails
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
>>> # Start monitoring after initiating update
|
|
297
|
+
>>> await device.start_firmware_update()
|
|
298
|
+
>>>
|
|
299
|
+
>>> # Poll for progress
|
|
300
|
+
>>> while True:
|
|
301
|
+
... progress = await device.get_firmware_update_progress()
|
|
302
|
+
... if not progress.in_progress:
|
|
303
|
+
... break
|
|
304
|
+
... print(f"Progress: {progress.update_percentage}%")
|
|
305
|
+
... await asyncio.sleep(30) # Poll every 30 seconds
|
|
306
|
+
"""
|
|
307
|
+
# Import here to avoid circular imports
|
|
308
|
+
import re
|
|
309
|
+
|
|
310
|
+
from pylxpweb.models import FirmwareUpdateInfo
|
|
311
|
+
|
|
312
|
+
client: LuxpowerClient = self._client # type: ignore[attr-defined]
|
|
313
|
+
serial: str = self.serial_number # type: ignore[attr-defined]
|
|
314
|
+
|
|
315
|
+
# Check cache (only if not forced)
|
|
316
|
+
# Note: We check cache age first, but if there's an active update,
|
|
317
|
+
# we need fresh data regardless of cache age. However, we can only
|
|
318
|
+
# know if there's an active update by checking the API, so we use
|
|
319
|
+
# a shorter TTL (30 seconds) to ensure we detect updates quickly
|
|
320
|
+
# while still reducing API load during normal operation.
|
|
321
|
+
if not force:
|
|
322
|
+
async with self._firmware_update_cache_lock:
|
|
323
|
+
if (
|
|
324
|
+
self._firmware_update_info is not None
|
|
325
|
+
and self._firmware_update_cache_time is not None
|
|
326
|
+
):
|
|
327
|
+
cache_age = datetime.now() - self._firmware_update_cache_time
|
|
328
|
+
|
|
329
|
+
# Use different cache TTLs based on update status
|
|
330
|
+
if self._firmware_update_info.in_progress:
|
|
331
|
+
# During active update: use very short cache (10 seconds)
|
|
332
|
+
# to get near real-time progress
|
|
333
|
+
cache_ttl = timedelta(seconds=10)
|
|
334
|
+
else:
|
|
335
|
+
# No active update: use longer cache (5 minutes)
|
|
336
|
+
# to reduce API load
|
|
337
|
+
cache_ttl = timedelta(minutes=5)
|
|
338
|
+
|
|
339
|
+
if cache_age < cache_ttl:
|
|
340
|
+
return self._firmware_update_info
|
|
341
|
+
|
|
342
|
+
# Get current update status from API
|
|
343
|
+
status = await client.api.firmware.get_firmware_update_status()
|
|
344
|
+
|
|
345
|
+
# Find this device's progress info
|
|
346
|
+
device_info = next(
|
|
347
|
+
(info for info in status.deviceInfos if info.inverterSn == serial),
|
|
348
|
+
None,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Determine progress state
|
|
352
|
+
in_progress = False
|
|
353
|
+
update_percentage: int | None = None
|
|
354
|
+
|
|
355
|
+
if device_info is not None:
|
|
356
|
+
# Check if update is in progress
|
|
357
|
+
in_progress = device_info.is_in_progress
|
|
358
|
+
|
|
359
|
+
# Parse percentage from updateRate string (e.g., "50% - 280 / 561")
|
|
360
|
+
if device_info.updateRate:
|
|
361
|
+
match = re.match(r"^(\d+)%", device_info.updateRate)
|
|
362
|
+
if match:
|
|
363
|
+
update_percentage = int(match.group(1))
|
|
364
|
+
|
|
365
|
+
# Get cached firmware check data (required for version info)
|
|
366
|
+
# If not cached, fetch it now
|
|
367
|
+
if self._firmware_update_info is None:
|
|
368
|
+
await self.check_firmware_updates()
|
|
369
|
+
assert self._firmware_update_info is not None
|
|
370
|
+
|
|
371
|
+
# Create updated FirmwareUpdateInfo with progress data
|
|
372
|
+
update_info = FirmwareUpdateInfo(
|
|
373
|
+
installed_version=self._firmware_update_info.installed_version,
|
|
374
|
+
latest_version=self._firmware_update_info.latest_version,
|
|
375
|
+
title=self._firmware_update_info.title,
|
|
376
|
+
release_summary=self._firmware_update_info.release_summary,
|
|
377
|
+
release_url=self._firmware_update_info.release_url,
|
|
378
|
+
in_progress=in_progress,
|
|
379
|
+
update_percentage=update_percentage,
|
|
380
|
+
device_class=self._firmware_update_info.device_class,
|
|
381
|
+
supported_features=self._firmware_update_info.supported_features,
|
|
382
|
+
app_version_current=self._firmware_update_info.app_version_current,
|
|
383
|
+
app_version_latest=self._firmware_update_info.app_version_latest,
|
|
384
|
+
param_version_current=self._firmware_update_info.param_version_current,
|
|
385
|
+
param_version_latest=self._firmware_update_info.param_version_latest,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Update cache with progress data
|
|
389
|
+
async with self._firmware_update_cache_lock:
|
|
390
|
+
self._firmware_update_info = update_info
|
|
391
|
+
# Update timestamp: allows caching when no active update
|
|
392
|
+
self._firmware_update_cache_time = datetime.now()
|
|
393
|
+
|
|
394
|
+
return update_info
|
|
395
|
+
|
|
396
|
+
async def start_firmware_update(self, try_fast_mode: bool = False) -> bool:
|
|
397
|
+
"""Start firmware update for this device.
|
|
398
|
+
|
|
399
|
+
⚠️ CRITICAL WARNING - WRITE OPERATION
|
|
400
|
+
This initiates an actual firmware update that:
|
|
401
|
+
- Takes 20-40 minutes to complete
|
|
402
|
+
- Makes device unavailable during update
|
|
403
|
+
- Requires uninterrupted power and network
|
|
404
|
+
- May brick device if interrupted
|
|
405
|
+
|
|
406
|
+
Recommended workflow:
|
|
407
|
+
1. Call check_firmware_updates() to verify update is available
|
|
408
|
+
2. Call check_update_eligibility() to verify device is ready
|
|
409
|
+
3. Get explicit user confirmation
|
|
410
|
+
4. Call this method to start update
|
|
411
|
+
5. Monitor progress with get_firmware_update_status()
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
try_fast_mode: Attempt fast update mode (may reduce time by 20-30%)
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Boolean indicating if update was initiated successfully
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
LuxpowerAuthError: If authentication fails
|
|
421
|
+
LuxpowerAPIError: If update cannot be started (already updating,
|
|
422
|
+
no update available, parallel group updating)
|
|
423
|
+
LuxpowerConnectionError: If connection fails
|
|
424
|
+
|
|
425
|
+
Example:
|
|
426
|
+
>>> # Check for updates first
|
|
427
|
+
>>> update_info = await device.check_firmware_updates()
|
|
428
|
+
>>> if not update_info.update_available:
|
|
429
|
+
... print("No update available")
|
|
430
|
+
... return
|
|
431
|
+
...
|
|
432
|
+
>>> # Check eligibility
|
|
433
|
+
>>> eligible = await device.check_update_eligibility()
|
|
434
|
+
>>> if not eligible:
|
|
435
|
+
... print("Device not eligible for update")
|
|
436
|
+
... return
|
|
437
|
+
...
|
|
438
|
+
>>> # Get user confirmation
|
|
439
|
+
>>> if confirm_with_user():
|
|
440
|
+
... success = await device.start_firmware_update()
|
|
441
|
+
... if success:
|
|
442
|
+
... print("Update started successfully")
|
|
443
|
+
"""
|
|
444
|
+
# Import here to avoid circular imports
|
|
445
|
+
from pylxpweb.models import FirmwareUpdateInfo
|
|
446
|
+
|
|
447
|
+
client: LuxpowerClient = self._client # type: ignore[attr-defined]
|
|
448
|
+
serial: str = self.serial_number # type: ignore[attr-defined]
|
|
449
|
+
|
|
450
|
+
# Start the firmware update
|
|
451
|
+
success = await client.api.firmware.start_firmware_update(
|
|
452
|
+
serial, try_fast_mode=try_fast_mode
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Optimistic update: If successful, immediately set in_progress=True
|
|
456
|
+
# This ensures cache bypass logic activates right away for progress tracking
|
|
457
|
+
if success and self._firmware_update_info is not None:
|
|
458
|
+
async with self._firmware_update_cache_lock:
|
|
459
|
+
# Create updated info with in_progress=True and initial 0% progress
|
|
460
|
+
self._firmware_update_info = FirmwareUpdateInfo(
|
|
461
|
+
installed_version=self._firmware_update_info.installed_version,
|
|
462
|
+
latest_version=self._firmware_update_info.latest_version,
|
|
463
|
+
title=self._firmware_update_info.title,
|
|
464
|
+
release_summary=self._firmware_update_info.release_summary,
|
|
465
|
+
release_url=self._firmware_update_info.release_url,
|
|
466
|
+
in_progress=True, # Optimistically set to True
|
|
467
|
+
update_percentage=0, # Start at 0%
|
|
468
|
+
device_class=self._firmware_update_info.device_class,
|
|
469
|
+
supported_features=self._firmware_update_info.supported_features,
|
|
470
|
+
app_version_current=self._firmware_update_info.app_version_current,
|
|
471
|
+
app_version_latest=self._firmware_update_info.app_version_latest,
|
|
472
|
+
param_version_current=self._firmware_update_info.param_version_current,
|
|
473
|
+
param_version_latest=self._firmware_update_info.param_version_latest,
|
|
474
|
+
)
|
|
475
|
+
# Update timestamp so next progress call uses 10-second cache
|
|
476
|
+
self._firmware_update_cache_time = datetime.now()
|
|
477
|
+
|
|
478
|
+
return success
|
|
479
|
+
|
|
480
|
+
async def check_update_eligibility(self) -> bool:
|
|
481
|
+
"""Check if this device is eligible for firmware update.
|
|
482
|
+
|
|
483
|
+
This is a READ-ONLY operation that verifies if the device can be updated.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
True if device is eligible for update, False otherwise
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
LuxpowerAuthError: If authentication fails
|
|
490
|
+
LuxpowerAPIError: If API check fails
|
|
491
|
+
LuxpowerConnectionError: If connection fails
|
|
492
|
+
|
|
493
|
+
Example:
|
|
494
|
+
>>> eligible = await device.check_update_eligibility()
|
|
495
|
+
>>> if eligible:
|
|
496
|
+
... await device.start_firmware_update()
|
|
497
|
+
>>> else:
|
|
498
|
+
... print("Device is not eligible for update (may be updating already)")
|
|
499
|
+
"""
|
|
500
|
+
client: LuxpowerClient = self._client # type: ignore[attr-defined]
|
|
501
|
+
serial: str = self.serial_number # type: ignore[attr-defined]
|
|
502
|
+
|
|
503
|
+
eligibility = await client.api.firmware.check_update_eligibility(serial)
|
|
504
|
+
return eligibility.is_allowed
|