55import asyncio
66import base64
77import binascii
8+ import gzip
89import hashlib
910import hmac
1011import json
1314import secrets
1415import struct
1516import time
16- from typing import Any
17+ from typing import Any , Callable
1718
1819import aiohttp
1920from Crypto .Cipher import AES
2021from Crypto .Util .Padding import pad , unpad
2122
2223from roborock .exceptions import (
23- RoborockException ,
24+ RoborockException , RoborockTimeout , VacuumError ,
2425)
2526from .code_mappings import WASH_MODE_MAP , DUST_COLLECTION_MAP , RoborockDockType , \
26- RoborockDockDustCollectionType , RoborockDockWashingModeType
27+ RoborockDockDustCollectionType , RoborockDockWashingModeType , STATE_CODE_TO_STATUS
2728from .containers import (
2829 UserData ,
29- HomeDataDevice ,
3030 Status ,
3131 CleanSummary ,
3232 Consumable ,
3737 SmartWashParameters ,
3838
3939)
40+ from .roborock_queue import RoborockQueue
4041from .typing import (
4142 RoborockDeviceProp ,
4243 RoborockCommand ,
@@ -86,7 +87,7 @@ async def request(
8687 return await resp .json ()
8788
8889
89- class RoborockClient () :
90+ class RoborockClient :
9091
9192 def __init__ (self , endpoint : str , device_localkey : dict [str , str ], prefixed = False ) -> None :
9293 self .device_localkey = device_localkey
@@ -97,6 +98,8 @@ def __init__(self, endpoint: str, device_localkey: dict[str, str], prefixed=Fals
9798 self ._endpoint = base64 .b64encode (md5bin (endpoint )[8 :14 ]).decode ()
9899 self ._nonce = secrets .token_bytes (16 )
99100 self ._prefixed = prefixed
101+ self ._waiting_queue : dict [int , RoborockQueue ] = {}
102+ self ._status_listeners : list [Callable [[str , str ], None ]] = []
100103
101104 def _decode_msg (self , msg : bytes , local_key : str ) -> dict [str , Any ]:
102105 if self ._prefixed :
@@ -112,13 +115,13 @@ def _decode_msg(self, msg: bytes, local_key: str) -> dict[str, Any]:
112115 "timestamp" : timestamp ,
113116 "protocol" : protocol ,
114117 }
115- crc32 = binascii .crc32 (msg [0 : len (msg ) - 4 ])
118+ # crc32 = binascii.crc32(msg[0: len(msg) - 4])
116119 [version , _seq , _random , timestamp , protocol , payload_len ] = struct .unpack (
117120 "!3sIIIHH" , msg [0 :19 ]
118121 )
119122 [payload , expected_crc32 ] = struct .unpack_from (f"!{ payload_len } sI" , msg , 19 )
120- if crc32 != expected_crc32 :
121- raise RoborockException (f"Wrong CRC32 { crc32 } , expected { expected_crc32 } " )
123+ # if crc32 != expected_crc32:
124+ # raise RoborockException(f"Wrong CRC32 {crc32}, expected {expected_crc32}")
122125
123126 aes_key = md5bin (encode_timestamp (timestamp ) + local_key + self ._salt )
124127 decipher = AES .new (aes_key , AES .MODE_ECB )
@@ -130,7 +133,7 @@ def _decode_msg(self, msg: bytes, local_key: str) -> dict[str, Any]:
130133 "payload" : decrypted_payload ,
131134 }
132135
133- def _get_msg_raw (self , device_id , protocol , timestamp , payload , prefix = '' ) -> bytes :
136+ def _encode_msg (self , device_id , protocol , timestamp , payload , prefix = '' ) -> bytes :
134137 local_key = self .device_localkey [device_id ]
135138 aes_key = md5bin (encode_timestamp (timestamp ) + local_key + self ._salt )
136139 cipher = AES .new (aes_key , AES .MODE_ECB )
@@ -155,6 +158,81 @@ def _get_msg_raw(self, device_id, protocol, timestamp, payload, prefix='') -> by
155158 msg += struct .pack ("!I" , crc32 )
156159 return msg
157160
161+ async def on_message (self , device_id , msg ) -> None :
162+ try :
163+ data = self ._decode_msg (msg , self .device_localkey [device_id ])
164+ protocol = data .get ("protocol" )
165+ if protocol == 102 or protocol == 4 :
166+ payload = json .loads (data .get ("payload" ).decode ())
167+ for data_point_number , data_point in payload .get ("dps" ).items ():
168+ if data_point_number == "102" :
169+ data_point_response = json .loads (data_point )
170+ request_id = data_point_response .get ("id" )
171+ queue = self ._waiting_queue .get (request_id )
172+ if queue :
173+ if queue .protocol == protocol :
174+ error = data_point_response .get ("error" )
175+ if error :
176+ await queue .async_put (
177+ (
178+ None ,
179+ VacuumError (
180+ error .get ("code" ), error .get ("message" )
181+ ),
182+ ),
183+ timeout = QUEUE_TIMEOUT ,
184+ )
185+ else :
186+ result = data_point_response .get ("result" )
187+ if isinstance (result , list ) and len (result ) > 0 :
188+ result = result [0 ]
189+ await queue .async_put (
190+ (result , None ), timeout = QUEUE_TIMEOUT
191+ )
192+ elif request_id < self ._id_counter :
193+ _LOGGER .debug (
194+ f"id={ request_id } Ignoring response: { data_point_response } "
195+ )
196+ elif data_point_number == "121" :
197+ status = STATE_CODE_TO_STATUS .get (data_point )
198+ _LOGGER .debug (f"Status updated to { status } " )
199+ for listener in self ._status_listeners :
200+ listener (device_id , status )
201+ else :
202+ _LOGGER .debug (
203+ f"Unknown data point number received { data_point_number } with { data_point } "
204+ )
205+ elif protocol == 301 :
206+ payload = data .get ("payload" )[0 :24 ]
207+ [endpoint , _ , request_id , _ ] = struct .unpack ("<15sBH6s" , payload )
208+ if endpoint .decode ().startswith (self ._endpoint ):
209+ iv = bytes (AES .block_size )
210+ decipher = AES .new (self ._nonce , AES .MODE_CBC , iv )
211+ decrypted = unpad (
212+ decipher .decrypt (data .get ("payload" )[24 :]), AES .block_size
213+ )
214+ decrypted = gzip .decompress (decrypted )
215+ queue = self ._waiting_queue .get (request_id )
216+ if queue :
217+ if isinstance (decrypted , list ):
218+ decrypted = decrypted [0 ]
219+ await queue .async_put ((decrypted , None ), timeout = QUEUE_TIMEOUT )
220+ except Exception as ex :
221+ _LOGGER .exception (ex )
222+
223+ async def _async_response (self , request_id : int , protocol_id : int = 0 ) -> tuple [Any , VacuumError | None ]:
224+ try :
225+ queue = RoborockQueue (protocol_id )
226+ self ._waiting_queue [request_id ] = queue
227+ (response , err ) = await queue .async_get (QUEUE_TIMEOUT )
228+ return response , err
229+ except (asyncio .TimeoutError , asyncio .CancelledError ):
230+ raise RoborockTimeout (
231+ f"Timeout after { QUEUE_TIMEOUT } seconds waiting for response"
232+ ) from None
233+ finally :
234+ del self ._waiting_queue [request_id ]
235+
158236 def _get_payload (
159237 self , method : RoborockCommand , params : list = None
160238 ):
0 commit comments