6
6
import logging
7
7
from collections .abc import Coroutine
8
8
from datetime import date
9
- from typing import TYPE_CHECKING , Any , Callable , Optional
9
+ from typing import TYPE_CHECKING , Callable , Optional
10
10
11
11
# When support for cpython older than 3.11 is dropped
12
12
# async_timeout can be replaced with asyncio.timeout
13
13
from async_timeout import timeout as asyncio_timeout
14
14
from pydantic .v1 import BaseModel , Field , validator
15
15
16
+ from ...exceptions import KasaException
16
17
from ...feature import Feature
17
18
from ..smartmodule import SmartModule , allow_update_after
18
19
@@ -70,6 +71,11 @@ class Firmware(SmartModule):
70
71
71
72
def __init__ (self , device : SmartDevice , module : str ):
72
73
super ().__init__ (device , module )
74
+ self ._firmware_update_info : UpdateInfo | None = None
75
+
76
+ def _initialize_features (self ):
77
+ """Initialize features."""
78
+ device = self ._device
73
79
if self .supported_version > 1 :
74
80
self ._add_feature (
75
81
Feature (
@@ -115,40 +121,58 @@ def __init__(self, device: SmartDevice, module: str):
115
121
type = Feature .Type .Sensor ,
116
122
)
117
123
)
124
+ self ._add_feature (
125
+ Feature (
126
+ device ,
127
+ id = "check_latest_firmware" ,
128
+ name = "Check latest firmware" ,
129
+ container = self ,
130
+ attribute_setter = "check_latest_firmware" ,
131
+ category = Feature .Category .Info ,
132
+ type = Feature .Type .Action ,
133
+ )
134
+ )
118
135
119
136
def query (self ) -> dict :
120
137
"""Query to execute during the update cycle."""
121
- req : dict [str , Any ] = {"get_latest_fw" : None }
122
138
if self .supported_version > 1 :
123
- req ["get_auto_update_info" ] = None
124
- return req
139
+ return {"get_auto_update_info" : None }
140
+ return {}
141
+
142
+ async def check_latest_firmware (self ) -> UpdateInfo | None :
143
+ """Check for the latest firmware for the device."""
144
+ try :
145
+ fw = await self .call ("get_latest_fw" )
146
+ self ._firmware_update_info = UpdateInfo .parse_obj (fw ["get_latest_fw" ])
147
+ return self ._firmware_update_info
148
+ except Exception :
149
+ _LOGGER .exception ("Error getting latest firmware for %s:" , self ._device )
150
+ self ._firmware_update_info = None
151
+ return None
125
152
126
153
@property
127
154
def current_firmware (self ) -> str :
128
155
"""Return the current firmware version."""
129
156
return self ._device .hw_info ["sw_ver" ]
130
157
131
158
@property
132
- def latest_firmware (self ) -> str :
159
+ def latest_firmware (self ) -> str | None :
133
160
"""Return the latest firmware version."""
134
- return self .firmware_update_info .version
161
+ if not self ._firmware_update_info :
162
+ return None
163
+ return self ._firmware_update_info .version
135
164
136
165
@property
137
- def firmware_update_info (self ):
166
+ def firmware_update_info (self ) -> UpdateInfo | None :
138
167
"""Return latest firmware information."""
139
- if not self ._device .is_cloud_connected or self ._has_data_error ():
140
- # Error in response, probably disconnected from the cloud.
141
- return UpdateInfo (type = 0 , need_to_upgrade = False )
142
-
143
- fw = self .data .get ("get_latest_fw" ) or self .data
144
- return UpdateInfo .parse_obj (fw )
168
+ return self ._firmware_update_info
145
169
146
170
@property
147
171
def update_available (self ) -> bool | None :
148
172
"""Return True if update is available."""
149
- if not self ._device .is_cloud_connected :
173
+ if not self ._device .is_cloud_connected or not self . _firmware_update_info :
150
174
return None
151
- return self .firmware_update_info .update_available
175
+ return self ._firmware_update_info .update_available
152
176
153
177
async def get_update_state (self ) -> DownloadState :
154
178
"""Return update state."""
@@ -161,11 +185,17 @@ async def update(
161
185
self , progress_cb : Callable [[DownloadState ], Coroutine ] | None = None
162
186
):
163
187
"""Update the device firmware."""
188
+ if not self ._firmware_update_info :
189
+ raise KasaException (
190
+ "You must call check_latest_firmware before calling update"
191
+ )
192
+ if not self .update_available :
193
+ raise KasaException ("A new update must be available to call update" )
164
194
current_fw = self .current_firmware
165
195
_LOGGER .info (
166
196
"Going to upgrade from %s to %s" ,
167
197
current_fw ,
168
- self .firmware_update_info .version ,
198
+ self ._firmware_update_info .version ,
169
199
)
170
200
await self .call ("fw_download" )
171
201
@@ -188,7 +218,7 @@ async def update(
188
218
if state .status == 0 :
189
219
_LOGGER .info (
190
220
"Update idle, hopefully updated to %s" ,
191
- self .firmware_update_info .version ,
221
+ self ._firmware_update_info .version ,
192
222
)
193
223
break
194
224
elif state .status == 2 :
@@ -207,15 +237,12 @@ async def update(
207
237
_LOGGER .warning ("Unhandled state code: %s" , state )
208
238
209
239
@property
210
- def auto_update_enabled (self ):
240
+ def auto_update_enabled (self ) -> bool :
211
241
"""Return True if autoupdate is enabled."""
212
- return (
213
- "get_auto_update_info" in self .data
214
- and self .data ["get_auto_update_info" ]["enable" ]
215
- )
242
+ return "enable" in self .data and self .data ["enable" ]
216
243
217
244
@allow_update_after
218
245
async def set_auto_update_enabled (self , enabled : bool ):
219
246
"""Change autoupdate setting."""
220
- data = {** self .data [ "get_auto_update_info" ] , "enable" : enabled }
247
+ data = {** self .data , "enable" : enabled }
221
248
await self .call ("set_auto_update_info" , data )
0 commit comments