5
5
import asyncio
6
6
import base64
7
7
import binascii
8
+ import gzip
8
9
import hashlib
9
10
import hmac
10
11
import json
13
14
import secrets
14
15
import struct
15
16
import time
16
- from typing import Any
17
+ from typing import Any , Callable
17
18
18
19
import aiohttp
19
20
from Crypto .Cipher import AES
20
21
from Crypto .Util .Padding import pad , unpad
21
22
22
23
from roborock .exceptions import (
23
- RoborockException ,
24
+ RoborockException , RoborockTimeout , VacuumError ,
24
25
)
25
26
from .code_mappings import WASH_MODE_MAP , DUST_COLLECTION_MAP , RoborockDockType , \
26
- RoborockDockDustCollectionType , RoborockDockWashingModeType
27
+ RoborockDockDustCollectionType , RoborockDockWashingModeType , STATE_CODE_TO_STATUS
27
28
from .containers import (
28
29
UserData ,
29
- HomeDataDevice ,
30
30
Status ,
31
31
CleanSummary ,
32
32
Consumable ,
37
37
SmartWashParameters ,
38
38
39
39
)
40
+ from .roborock_queue import RoborockQueue
40
41
from .typing import (
41
42
RoborockDeviceProp ,
42
43
RoborockCommand ,
@@ -86,7 +87,7 @@ async def request(
86
87
return await resp .json ()
87
88
88
89
89
- class RoborockClient () :
90
+ class RoborockClient :
90
91
91
92
def __init__ (self , endpoint : str , device_localkey : dict [str , str ], prefixed = False ) -> None :
92
93
self .device_localkey = device_localkey
@@ -97,6 +98,8 @@ def __init__(self, endpoint: str, device_localkey: dict[str, str], prefixed=Fals
97
98
self ._endpoint = base64 .b64encode (md5bin (endpoint )[8 :14 ]).decode ()
98
99
self ._nonce = secrets .token_bytes (16 )
99
100
self ._prefixed = prefixed
101
+ self ._waiting_queue : dict [int , RoborockQueue ] = {}
102
+ self ._status_listeners : list [Callable [[str , str ], None ]] = []
100
103
101
104
def _decode_msg (self , msg : bytes , local_key : str ) -> dict [str , Any ]:
102
105
if self ._prefixed :
@@ -112,13 +115,13 @@ def _decode_msg(self, msg: bytes, local_key: str) -> dict[str, Any]:
112
115
"timestamp" : timestamp ,
113
116
"protocol" : protocol ,
114
117
}
115
- crc32 = binascii .crc32 (msg [0 : len (msg ) - 4 ])
118
+ # crc32 = binascii.crc32(msg[0: len(msg) - 4])
116
119
[version , _seq , _random , timestamp , protocol , payload_len ] = struct .unpack (
117
120
"!3sIIIHH" , msg [0 :19 ]
118
121
)
119
122
[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}")
122
125
123
126
aes_key = md5bin (encode_timestamp (timestamp ) + local_key + self ._salt )
124
127
decipher = AES .new (aes_key , AES .MODE_ECB )
@@ -130,7 +133,7 @@ def _decode_msg(self, msg: bytes, local_key: str) -> dict[str, Any]:
130
133
"payload" : decrypted_payload ,
131
134
}
132
135
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 :
134
137
local_key = self .device_localkey [device_id ]
135
138
aes_key = md5bin (encode_timestamp (timestamp ) + local_key + self ._salt )
136
139
cipher = AES .new (aes_key , AES .MODE_ECB )
@@ -155,6 +158,81 @@ def _get_msg_raw(self, device_id, protocol, timestamp, payload, prefix='') -> by
155
158
msg += struct .pack ("!I" , crc32 )
156
159
return msg
157
160
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
+
158
236
def _get_payload (
159
237
self , method : RoborockCommand , params : list = None
160
238
):
0 commit comments