1414import struct
1515import time
1616from random import randint
17- from typing import Any , Callable , Coroutine , Mapping , Optional
17+ from typing import Any , Callable , Coroutine , Optional
1818
1919import aiohttp
2020from Crypto .Cipher import AES
4848)
4949from .roborock_future import RoborockFuture
5050from .roborock_message import RoborockMessage
51- from .typing import DeviceProp , DockSummary , RoborockCommand
51+ from .roborock_typing import DeviceProp , DockSummary , RoborockCommand
5252from .util import unpack_list
5353
5454_LOGGER = logging .getLogger (__name__ )
5555KEEPALIVE = 60
5656QUEUE_TIMEOUT = 4
57- SPECIAL_COMMANDS = [
57+ COMMANDS_SECURED = [
5858 RoborockCommand .GET_MAP_V1 ,
5959]
6060
@@ -85,15 +85,18 @@ async def request(self, method: str, url: str, params=None, data=None, headers=N
8585
8686
8787class RoborockClient :
88- def __init__ (self , endpoint : str , devices_info : Mapping [ str , RoborockDeviceInfo ] ) -> None :
89- self .devices_info = devices_info
88+ def __init__ (self , endpoint : str , device_info : RoborockDeviceInfo ) -> None :
89+ self .device_info = device_info
9090 self ._endpoint = endpoint
9191 self ._nonce = secrets .token_bytes (16 )
9292 self ._waiting_queue : dict [int , RoborockFuture ] = {}
9393 self ._last_device_msg_in = self .time_func ()
9494 self ._last_disconnection = self .time_func ()
9595 self .keep_alive = KEEPALIVE
9696
97+ def __del__ (self ) -> None :
98+ self .sync_disconnect ()
99+
97100 @property
98101 def time_func (self ) -> Callable [[], float ]:
99102 try :
@@ -103,10 +106,16 @@ def time_func(self) -> Callable[[], float]:
103106 time_func = time .time
104107 return time_func
105108
109+ async def async_connect (self ):
110+ raise NotImplementedError
111+
112+ def sync_disconnect (self ) -> Any :
113+ raise NotImplementedError
114+
106115 async def async_disconnect (self ) -> Any :
107116 raise NotImplementedError
108117
109- def on_message (self , messages : list [RoborockMessage ]) -> None :
118+ def on_message_received (self , messages : list [RoborockMessage ]) -> None :
110119 try :
111120 self ._last_device_msg_in = self .time_func ()
112121 for data in messages :
@@ -118,24 +127,23 @@ def on_message(self, messages: list[RoborockMessage]) -> None:
118127 data_point_response = json .loads (data_point )
119128 request_id = data_point_response .get ("id" )
120129 queue = self ._waiting_queue .get (request_id )
121- if queue :
122- if queue .protocol == protocol :
123- error = data_point_response .get ("error" )
124- if error :
125- queue .resolve (
126- (
127- None ,
128- VacuumError (
129- error .get ("code" ),
130- error .get ("message" ),
131- ),
132- )
130+ if queue and queue .protocol == protocol :
131+ error = data_point_response .get ("error" )
132+ if error :
133+ queue .resolve (
134+ (
135+ None ,
136+ VacuumError (
137+ error .get ("code" ),
138+ error .get ("message" ),
139+ ),
133140 )
134- else :
135- result = data_point_response .get ("result" )
136- if isinstance (result , list ) and len (result ) == 1 :
137- result = result [0 ]
138- queue .resolve ((result , None ))
141+ )
142+ else :
143+ result = data_point_response .get ("result" )
144+ if isinstance (result , list ) and len (result ) == 1 :
145+ result = result [0 ]
146+ queue .resolve ((result , None ))
139147 elif protocol == 301 :
140148 payload = data .payload [0 :24 ]
141149 [endpoint , _ , request_id , _ ] = struct .unpack ("<15sBH6s" , payload )
@@ -149,10 +157,14 @@ def on_message(self, messages: list[RoborockMessage]) -> None:
149157 if isinstance (decrypted , list ):
150158 decrypted = decrypted [0 ]
151159 queue .resolve ((decrypted , None ))
160+ else :
161+ queue = self ._waiting_queue .get (data .seq )
162+ if queue :
163+ queue .resolve ((data .payload , None ))
152164 except Exception as ex :
153165 _LOGGER .exception (ex )
154166
155- def on_disconnect (self , exc : Optional [Exception ]) -> None :
167+ def on_connection_lost (self , exc : Optional [Exception ]) -> None :
156168 self ._last_disconnection = self .time_func ()
157169 _LOGGER .warning ("Roborock client disconnected" )
158170 if exc is not None :
@@ -165,6 +177,11 @@ def should_keepalive(self) -> bool:
165177 return False
166178 return True
167179
180+ async def validate_connection (self ) -> None :
181+ if not self .should_keepalive ():
182+ await self .async_disconnect ()
183+ await self .async_connect ()
184+
168185 async def _async_response (self , request_id : int , protocol_id : int = 0 ) -> tuple [Any , VacuumError | None ]:
169186 try :
170187 queue = RoborockFuture (protocol_id )
@@ -176,7 +193,7 @@ async def _async_response(self, request_id: int, protocol_id: int = 0) -> tuple[
176193 finally :
177194 del self ._waiting_queue [request_id ]
178195
179- def _get_payload (self , method : RoborockCommand , params : Optional [list ] = None , secured = False ):
196+ def _get_payload (self , method : RoborockCommand , params : Optional [list | dict ] = None , secured = False ):
180197 timestamp = math .floor (time .time ())
181198 request_id = randint (10000 , 99999 )
182199 inner = {
@@ -200,27 +217,27 @@ def _get_payload(self, method: RoborockCommand, params: Optional[list] = None, s
200217 )
201218 return request_id , timestamp , payload
202219
203- async def send_command (self , device_id : str , method : RoborockCommand , params : Optional [list ] = None ):
220+ async def send_command (self , method : RoborockCommand , params : Optional [list | dict ] = None ):
204221 raise NotImplementedError
205222
206- async def get_status (self , device_id : str ) -> Status | None :
207- status = await self .send_command (device_id , RoborockCommand .GET_STATUS )
223+ async def get_status (self ) -> Status | None :
224+ status = await self .send_command (RoborockCommand .GET_STATUS )
208225 if isinstance (status , dict ):
209226 return Status .from_dict (status )
210227 return None
211228
212- async def get_dnd_timer (self , device_id : str ) -> DNDTimer | None :
229+ async def get_dnd_timer (self ) -> DNDTimer | None :
213230 try :
214- dnd_timer = await self .send_command (device_id , RoborockCommand .GET_DND_TIMER )
231+ dnd_timer = await self .send_command (RoborockCommand .GET_DND_TIMER )
215232 if isinstance (dnd_timer , dict ):
216233 return DNDTimer .from_dict (dnd_timer )
217234 except RoborockTimeout as e :
218235 _LOGGER .error (e )
219236 return None
220237
221- async def get_clean_summary (self , device_id : str ) -> CleanSummary | None :
238+ async def get_clean_summary (self ) -> CleanSummary | None :
222239 try :
223- clean_summary = await self .send_command (device_id , RoborockCommand .GET_CLEAN_SUMMARY )
240+ clean_summary = await self .send_command (RoborockCommand .GET_CLEAN_SUMMARY )
224241 if isinstance (clean_summary , dict ):
225242 return CleanSummary .from_dict (clean_summary )
226243 elif isinstance (clean_summary , list ):
@@ -232,55 +249,54 @@ async def get_clean_summary(self, device_id: str) -> CleanSummary | None:
232249 _LOGGER .error (e )
233250 return None
234251
235- async def get_clean_record (self , device_id : str , record_id : int ) -> CleanRecord | None :
252+ async def get_clean_record (self , record_id : int ) -> CleanRecord | None :
236253 try :
237- clean_record = await self .send_command (device_id , RoborockCommand .GET_CLEAN_RECORD , [record_id ])
254+ clean_record = await self .send_command (RoborockCommand .GET_CLEAN_RECORD , [record_id ])
238255 if isinstance (clean_record , dict ):
239256 return CleanRecord .from_dict (clean_record )
240257 except RoborockTimeout as e :
241258 _LOGGER .error (e )
242259 return None
243260
244- async def get_consumable (self , device_id : str ) -> Consumable | None :
261+ async def get_consumable (self ) -> Consumable | None :
245262 try :
246- consumable = await self .send_command (device_id , RoborockCommand .GET_CONSUMABLE )
263+ consumable = await self .send_command (RoborockCommand .GET_CONSUMABLE )
247264 if isinstance (consumable , dict ):
248265 return Consumable .from_dict (consumable )
249266 except RoborockTimeout as e :
250267 _LOGGER .error (e )
251268 return None
252269
253- async def get_wash_towel_mode (self , device_id : str ) -> WashTowelMode | None :
270+ async def get_wash_towel_mode (self ) -> WashTowelMode | None :
254271 try :
255- washing_mode = await self .send_command (device_id , RoborockCommand .GET_WASH_TOWEL_MODE )
272+ washing_mode = await self .send_command (RoborockCommand .GET_WASH_TOWEL_MODE )
256273 if isinstance (washing_mode , dict ):
257274 return WashTowelMode .from_dict (washing_mode )
258275 except RoborockTimeout as e :
259276 _LOGGER .error (e )
260277 return None
261278
262- async def get_dust_collection_mode (self , device_id : str ) -> DustCollectionMode | None :
279+ async def get_dust_collection_mode (self ) -> DustCollectionMode | None :
263280 try :
264- dust_collection = await self .send_command (device_id , RoborockCommand .GET_DUST_COLLECTION_MODE )
281+ dust_collection = await self .send_command (RoborockCommand .GET_DUST_COLLECTION_MODE )
265282 if isinstance (dust_collection , dict ):
266283 return DustCollectionMode .from_dict (dust_collection )
267284 except RoborockTimeout as e :
268285 _LOGGER .error (e )
269286 return None
270287
271- async def get_smart_wash_params (self , device_id : str ) -> SmartWashParams | None :
288+ async def get_smart_wash_params (self ) -> SmartWashParams | None :
272289 try :
273- mop_wash_mode = await self .send_command (device_id , RoborockCommand .GET_SMART_WASH_PARAMS )
290+ mop_wash_mode = await self .send_command (RoborockCommand .GET_SMART_WASH_PARAMS )
274291 if isinstance (mop_wash_mode , dict ):
275292 return SmartWashParams .from_dict (mop_wash_mode )
276293 except RoborockTimeout as e :
277294 _LOGGER .error (e )
278295 return None
279296
280- async def get_dock_summary (self , device_id : str , dock_type : RoborockEnum ) -> DockSummary | None :
297+ async def get_dock_summary (self , dock_type : RoborockEnum ) -> DockSummary | None :
281298 """Gets the status summary from the dock with the methods available for a given dock.
282299
283- :param device_id: Device id
284300 :param dock_type: RoborockDockTypeCode"""
285301 try :
286302 commands : list [
@@ -289,11 +305,11 @@ async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> Doc
289305 Any ,
290306 DustCollectionMode | WashTowelMode | SmartWashParams | None ,
291307 ]
292- ] = [self .get_dust_collection_mode (device_id )]
308+ ] = [self .get_dust_collection_mode ()]
293309 if dock_type == RoborockDockTypeCode ["3" ]:
294310 commands += [
295- self .get_wash_towel_mode (device_id ),
296- self .get_smart_wash_params (device_id ),
311+ self .get_wash_towel_mode (),
312+ self .get_smart_wash_params (),
297313 ]
298314 [dust_collection_mode , wash_towel_mode , smart_wash_params ] = unpack_list (
299315 list (await asyncio .gather (* commands )), 3
@@ -304,21 +320,21 @@ async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> Doc
304320 _LOGGER .error (e )
305321 return None
306322
307- async def get_prop (self , device_id : str ) -> DeviceProp | None :
323+ async def get_prop (self ) -> DeviceProp | None :
308324 [status , dnd_timer , clean_summary , consumable ] = await asyncio .gather (
309325 * [
310- self .get_status (device_id ),
311- self .get_dnd_timer (device_id ),
312- self .get_clean_summary (device_id ),
313- self .get_consumable (device_id ),
326+ self .get_status (),
327+ self .get_dnd_timer (),
328+ self .get_clean_summary (),
329+ self .get_consumable (),
314330 ]
315331 )
316332 last_clean_record = None
317333 if clean_summary and clean_summary .records and len (clean_summary .records ) > 0 :
318- last_clean_record = await self .get_clean_record (device_id , clean_summary .records [0 ])
334+ last_clean_record = await self .get_clean_record (clean_summary .records [0 ])
319335 dock_summary = None
320336 if status and status .dock_type is not None and status .dock_type != RoborockDockTypeCode ["0" ]:
321- dock_summary = await self .get_dock_summary (device_id , status .dock_type )
337+ dock_summary = await self .get_dock_summary (status .dock_type )
322338 if any ([status , dnd_timer , clean_summary , consumable ]):
323339 return DeviceProp (
324340 status ,
@@ -330,27 +346,27 @@ async def get_prop(self, device_id: str) -> DeviceProp | None:
330346 )
331347 return None
332348
333- async def get_multi_maps_list (self , device_id ) -> MultiMapsList | None :
349+ async def get_multi_maps_list (self ) -> MultiMapsList | None :
334350 try :
335- multi_maps_list = await self .send_command (device_id , RoborockCommand .GET_MULTI_MAPS_LIST )
351+ multi_maps_list = await self .send_command (RoborockCommand .GET_MULTI_MAPS_LIST )
336352 if isinstance (multi_maps_list , dict ):
337353 return MultiMapsList .from_dict (multi_maps_list )
338354 except RoborockTimeout as e :
339355 _LOGGER .error (e )
340356 return None
341357
342- async def get_networking (self , device_id ) -> NetworkInfo | None :
358+ async def get_networking (self ) -> NetworkInfo | None :
343359 try :
344- networking_info = await self .send_command (device_id , RoborockCommand .GET_NETWORK_INFO )
360+ networking_info = await self .send_command (RoborockCommand .GET_NETWORK_INFO )
345361 if isinstance (networking_info , dict ):
346362 return NetworkInfo .from_dict (networking_info )
347363 except RoborockTimeout as e :
348364 _LOGGER .error (e )
349365 return None
350366
351- async def get_room_mapping (self , device_id : str ) -> list [RoomMapping ]:
367+ async def get_room_mapping (self ) -> list [RoomMapping ]:
352368 """Gets the mapping from segment id -> iot id. Only works on local api."""
353- mapping = await self .send_command (device_id , RoborockCommand .GET_ROOM_MAPPING )
369+ mapping = await self .send_command (RoborockCommand .GET_ROOM_MAPPING )
354370 if isinstance (mapping , list ):
355371 return [
356372 RoomMapping (segment_id = segment_id , iot_id = iot_id ) # type: ignore
0 commit comments