Skip to content

Commit e3c419c

Browse files
fix(local_api): receiving multiple messages
1 parent 7575aee commit e3c419c

File tree

6 files changed

+190
-152
lines changed

6 files changed

+190
-152
lines changed

roborock/api.py

Lines changed: 112 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
CleanRecord,
3535
HomeData,
3636
MultiMapsList,
37-
SmartWashParameters,
38-
RoborockDeviceInfo, WashTowelMode, DustCollectionMode,
37+
SmartWashParams,
38+
RoborockDeviceInfo,
39+
WashTowelMode,
40+
DustCollectionMode,
3941

4042
)
4143
from .roborock_queue import RoborockQueue
@@ -110,41 +112,51 @@ def add_status_listener(self, callback: Callable[[str, str], None]):
110112
async def async_disconnect(self) -> Any:
111113
raise NotImplementedError
112114

113-
def _decode_msg(self, msg: bytes, local_key: str) -> dict[str, Any]:
115+
def _decode_msg(self, msg: bytes, local_key: str) -> list[dict[str, Any]]:
116+
prefix = None
114117
if msg[4:7] == "1.0".encode():
118+
prefix = int.from_bytes(msg[:4], 'big')
115119
msg = msg[4:]
116120
elif msg[0:3] != "1.0".encode():
117121
raise RoborockException(f"Unknown protocol version {msg[0:3]}")
118-
if len(msg) == 17:
119-
[version, request_id, _random, timestamp, protocol] = struct.unpack(
122+
if len(msg) in [17, 21, 25]:
123+
[version, request_id, random, timestamp, protocol] = struct.unpack(
120124
"!3sIIIH", msg[0:17]
121125
)
122-
return {
126+
return [{
127+
"prefix": prefix,
123128
"version": version,
124129
"request_id": request_id,
130+
"random": random,
125131
"timestamp": timestamp,
126132
"protocol": protocol,
127-
}
128-
[version, request_id, _random, timestamp, protocol, payload_len] = struct.unpack(
129-
"!3sIIIHH", msg[0:19]
133+
}]
134+
index = 0
135+
[version, request_id, random, timestamp, protocol, payload_len] = struct.unpack(
136+
"!3sIIIHH", msg[index:index + 19]
130137
)
131-
extra_len = len(msg) - 23 - payload_len
132-
[payload, expected_crc32, extra] = struct.unpack_from(f"!{payload_len}sI{extra_len}s", msg, 19)
133-
if not extra_len:
134-
crc32 = binascii.crc32(msg[0: 19 + payload_len])
138+
[payload, expected_crc32] = struct.unpack_from(f"!{payload_len}sI", msg, index + 19)
139+
if payload_len == 0:
140+
index += 21
141+
else:
142+
crc32 = binascii.crc32(msg[index: index + 19 + payload_len])
143+
index += 23 + payload_len
135144
if crc32 != expected_crc32:
136145
raise RoborockException(f"Wrong CRC32 {crc32}, expected {expected_crc32}")
137-
138-
aes_key = md5bin(encode_timestamp(timestamp) + local_key + self._salt)
139-
decipher = AES.new(aes_key, AES.MODE_ECB)
140-
decrypted_payload = unpad(decipher.decrypt(payload), AES.block_size) if payload else extra
141-
return {
146+
decrypted_payload = None
147+
if payload:
148+
aes_key = md5bin(encode_timestamp(timestamp) + local_key + self._salt)
149+
decipher = AES.new(aes_key, AES.MODE_ECB)
150+
decrypted_payload = unpad(decipher.decrypt(payload), AES.block_size)
151+
return [{
152+
"prefix": prefix,
142153
"version": version,
143154
"request_id": request_id,
155+
"random": random,
144156
"timestamp": timestamp,
145157
"protocol": protocol,
146158
"payload": decrypted_payload
147-
}
159+
}] + (self._decode_msg(msg[index:], local_key) if index < len(msg) else [])
148160

149161
def _encode_msg(self, device_id, request_id, protocol, timestamp, payload, prefix=None) -> bytes:
150162
local_key = self.devices_info[device_id].device.local_key
@@ -171,67 +183,77 @@ def _encode_msg(self, device_id, request_id, protocol, timestamp, payload, prefi
171183
msg += struct.pack("!I", crc32)
172184
return msg
173185

174-
async def on_message(self, device_id, msg) -> bool:
186+
async def on_message(self, device_id, msg) -> None:
175187
try:
176-
data = self._decode_msg(msg, self.devices_info[device_id].device.local_key)
177-
protocol = data.get("protocol")
178-
if protocol == 102 or protocol == 4:
179-
payload = json.loads(data.get("payload").decode())
180-
for data_point_number, data_point in payload.get("dps").items():
181-
if data_point_number == "102":
182-
data_point_response = json.loads(data_point)
183-
request_id = data_point_response.get("id")
184-
queue = self._waiting_queue.get(request_id)
185-
if queue:
186-
if queue.protocol == protocol:
187-
error = data_point_response.get("error")
188-
if error:
189-
await queue.async_put(
190-
(
191-
None,
192-
VacuumError(
193-
error.get("code"), error.get("message")
188+
messages = self._decode_msg(msg, self.devices_info[device_id].device.local_key)
189+
for data in messages:
190+
protocol = data.get("protocol")
191+
if protocol == 102 or protocol == 4:
192+
payload = json.loads(data.get("payload").decode())
193+
for data_point_number, data_point in payload.get("dps").items():
194+
if data_point_number == "102":
195+
data_point_response = json.loads(data_point)
196+
request_id = data_point_response.get("id")
197+
queue = self._waiting_queue.get(request_id)
198+
if queue:
199+
if queue.protocol == protocol:
200+
error = data_point_response.get("error")
201+
if error:
202+
await queue.async_put(
203+
(
204+
None,
205+
VacuumError(
206+
error.get("code"), error.get("message")
207+
),
194208
),
195-
),
196-
timeout=QUEUE_TIMEOUT,
197-
)
198-
else:
199-
result = data_point_response.get("result")
200-
if isinstance(result, list) and len(result) == 1:
201-
result = result[0]
202-
await queue.async_put(
203-
(result, None), timeout=QUEUE_TIMEOUT
204-
)
205-
return True
206-
elif request_id < self._id_counter:
209+
timeout=QUEUE_TIMEOUT,
210+
)
211+
else:
212+
result = data_point_response.get("result")
213+
if isinstance(result, list) and len(result) == 1:
214+
result = result[0]
215+
await queue.async_put(
216+
(result, None), timeout=QUEUE_TIMEOUT
217+
)
218+
elif request_id < self._id_counter:
219+
_LOGGER.debug(
220+
f"id={request_id} Ignoring response: {data_point_response}"
221+
)
222+
elif data_point_number == "121":
223+
status = STATE_CODE_TO_STATUS.get(data_point)
224+
_LOGGER.debug(f"Status updated to {status}")
225+
for listener in self._status_listeners:
226+
listener(device_id, status)
227+
else:
207228
_LOGGER.debug(
208-
f"id={request_id} Ignoring response: {data_point_response}"
229+
f"Unknown data point number received {data_point_number} with {data_point}"
209230
)
210-
elif data_point_number == "121":
211-
status = STATE_CODE_TO_STATUS.get(data_point)
212-
_LOGGER.debug(f"Status updated to {status}")
213-
for listener in self._status_listeners:
214-
listener(device_id, status)
215-
else:
216-
_LOGGER.debug(
217-
f"Unknown data point number received {data_point_number} with {data_point}"
231+
elif protocol == 301:
232+
payload = data.get("payload")[0:24]
233+
[endpoint, _, request_id, _] = struct.unpack("<15sBH6s", payload)
234+
if endpoint.decode().startswith(self._endpoint):
235+
iv = bytes(AES.block_size)
236+
decipher = AES.new(self._nonce, AES.MODE_CBC, iv)
237+
decrypted = unpad(
238+
decipher.decrypt(data.get("payload")[24:]), AES.block_size
218239
)
219-
elif protocol == 301:
220-
payload = data.get("payload")[0:24]
221-
[endpoint, _, request_id, _] = struct.unpack("<15sBH6s", payload)
222-
if endpoint.decode().startswith(self._endpoint):
223-
iv = bytes(AES.block_size)
224-
decipher = AES.new(self._nonce, AES.MODE_CBC, iv)
225-
decrypted = unpad(
226-
decipher.decrypt(data.get("payload")[24:]), AES.block_size
227-
)
228-
decrypted = gzip.decompress(decrypted)
240+
decrypted = gzip.decompress(decrypted)
241+
queue = self._waiting_queue.get(request_id)
242+
if queue:
243+
if isinstance(decrypted, list):
244+
decrypted = decrypted[0]
245+
await queue.async_put((decrypted, None), timeout=QUEUE_TIMEOUT)
246+
elif data.get('request_id'):
247+
request_id = data.get('request_id')
229248
queue = self._waiting_queue.get(request_id)
230249
if queue:
231-
if isinstance(decrypted, list):
232-
decrypted = decrypted[0]
233-
await queue.async_put((decrypted, None), timeout=QUEUE_TIMEOUT)
234-
return True
250+
protocol = data.get("protocol")
251+
if queue.protocol == protocol:
252+
await queue.async_put((None, None), timeout=QUEUE_TIMEOUT)
253+
elif request_id < self._id_counter and protocol != 5:
254+
_LOGGER.debug(
255+
f"id={request_id} Ignoring response: {data}"
256+
)
235257
except Exception as ex:
236258
_LOGGER.exception(ex)
237259

@@ -262,13 +284,13 @@ def _get_payload(
262284
if secured:
263285
inner["security"] = {
264286
"endpoint": self._endpoint,
265-
"nonce": self._nonce.hex().upper(),
287+
"nonce": self._nonce.hex().lower(),
266288
}
267289
payload = bytes(
268290
json.dumps(
269291
{
270-
"t": timestamp,
271292
"dps": {"101": json.dumps(inner, separators=(",", ":"))},
293+
"t": timestamp,
272294
},
273295
separators=(",", ":"),
274296
).encode()
@@ -323,7 +345,7 @@ async def get_consumable(self, device_id: str) -> Consumable:
323345
except RoborockTimeout as e:
324346
_LOGGER.error(e)
325347

326-
async def get_washing_mode(self, device_id: str) -> WashTowelMode:
348+
async def get_wash_towel_mode(self, device_id: str) -> WashTowelMode:
327349
try:
328350
washing_mode = await self.send_command(device_id, RoborockCommand.GET_WASH_TOWEL_MODE)
329351
if isinstance(washing_mode, dict):
@@ -339,22 +361,22 @@ async def get_dust_collection_mode(self, device_id: str) -> DustCollectionMode:
339361
except RoborockTimeout as e:
340362
_LOGGER.error(e)
341363

342-
async def get_mop_wash_mode(self, device_id: str) -> SmartWashParameters:
364+
async def get_smart_wash_params(self, device_id: str) -> SmartWashParams:
343365
try:
344366
mop_wash_mode = await self.send_command(device_id, RoborockCommand.GET_SMART_WASH_PARAMS)
345367
if isinstance(mop_wash_mode, dict):
346-
return SmartWashParameters(mop_wash_mode)
368+
return SmartWashParams(mop_wash_mode)
347369
except RoborockTimeout as e:
348370
_LOGGER.error(e)
349371

350372
async def get_dock_summary(self, device_id: str, dock_type: RoborockDockType) -> RoborockDockSummary:
351373
try:
352374
commands = [self.get_dust_collection_mode(device_id)]
353375
if dock_type == RoborockDockType.EMPTY_WASH_FILL_DOCK:
354-
commands += [self.get_mop_wash_mode(device_id), self.get_washing_mode(device_id)]
355-
[collection_mode, mop_wash, washing_mode] = (list(await asyncio.gather(*commands)) + [None, None])[:3]
376+
commands += [self.get_wash_towel_mode(device_id), self.get_smart_wash_params(device_id)]
377+
[dust_collection_mode, wash_towel_mode, smart_wash_params] = (list(await asyncio.gather(*commands)) + [None, None])[:3]
356378

357-
return RoborockDockSummary(collection_mode, washing_mode, mop_wash)
379+
return RoborockDockSummary(dust_collection_mode, wash_towel_mode, smart_wash_params)
358380
except RoborockTimeout as e:
359381
_LOGGER.error(e)
360382

@@ -367,9 +389,17 @@ async def get_prop(self, device_id: str) -> RoborockDeviceProp:
367389
self.get_consumable(device_id),
368390
]
369391
)
392+
last_clean_record = None
393+
if clean_summary and clean_summary.records and len(clean_summary.records) > 0:
394+
last_clean_record = await self.get_clean_record(
395+
device_id, clean_summary.records[0]
396+
)
397+
dock_summary = None
398+
if status and status.dock_type != RoborockDockType.NO_DOCK:
399+
dock_summary = await self.get_dock_summary(device_id, status.dock_type)
370400
if any([status, dnd_timer, clean_summary, consumable]):
371401
return RoborockDeviceProp(
372-
status, dnd_timer, clean_summary, consumable
402+
status, dnd_timer, clean_summary, consumable, last_clean_record, dock_summary
373403
)
374404

375405
async def get_multi_maps_list(self, device_id) -> MultiMapsList:

roborock/cloud_api.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,3 @@ async def send_command(
205205
else:
206206
_LOGGER.debug(f"id={request_id} Response from {method}: {response}")
207207
return response
208-
209-
async def get_prop(self, device_id: str) -> RoborockDeviceProp:
210-
device_prop = await super().get_prop(device_id)
211-
last_clean_record = None
212-
if device_prop.clean_summary and device_prop.clean_summary.records and len(device_prop.clean_summary.records) > 0:
213-
last_clean_record = await self.get_clean_record(
214-
device_id, device_prop.clean_summary.records[0]
215-
)
216-
device_prop.last_clean_record = last_clean_record
217-
dock_summary = None
218-
if device_prop.status and device_prop.status.dock_type != RoborockDockType.NO_DOCK:
219-
dock_summary = await self.get_dock_summary(device_id, device_prop.status.dock_type)
220-
device_prop.dock_summary = dock_summary
221-
return device_prop

roborock/containers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ def map_info(self) -> list[MultiMapsListMapInfo]:
10251025
return [MultiMapsListMapInfo(map_info) for map_info in self.get(MultiMapListField.MAP_INFO)]
10261026

10271027

1028-
class SmartWashParameters(RoborockBase):
1028+
class SmartWashParams(RoborockBase):
10291029

10301030
@property
10311031
def smart_wash(self) -> int:

0 commit comments

Comments
 (0)