Skip to content

Commit 71f7f22

Browse files
chore: using python construct for data parsing (#48)
* chore: using python construct for data parsing * chore: linting * chore: linting * fix: roborock message protocol * fix: change local api constructor * chore: linting * chore: linting
1 parent 341e4fa commit 71f7f22

File tree

9 files changed

+233
-194
lines changed

9 files changed

+233
-194
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ repos:
4040
args:
4141
- --fix
4242
- repo: https://github.com/pre-commit/mirrors-mypy
43-
rev: v0.931
43+
rev: v1.2.0
4444
hooks:
4545
- id: mypy
4646
exclude: cli.py

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ roborock = "roborock.cli:main"
2121
[tool.poetry.dependencies]
2222
python = "^3.9"
2323
click = ">=8"
24-
aiohttp = "*"
24+
aiohttp = "^3.8.2"
2525
async-timeout = "*"
2626
pycryptodome = "~3.17.0"
2727
pycryptodomex = {version = "~3.17.0", markers = "sys_platform == 'darwin'"}
@@ -37,12 +37,13 @@ build-backend = "poetry.core.masonry.api"
3737
[tool.poetry.dev-dependencies]
3838
pytest-asyncio = "*"
3939
pytest = "*"
40-
pre-commit = "*"
40+
pre-commit = "^3.2.2"
4141
mypy = "*"
4242
ruff = "*"
4343
isort = "*"
4444
black = "*"
4545
codespell = "*"
46+
pyshark = "^0.6"
4647

4748
[tool.semantic_release]
4849
branch = "main"

roborock/api.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import asyncio
66
import base64
7-
import gzip
87
import hashlib
98
import hmac
109
import json
@@ -17,8 +16,6 @@
1716
from typing import Any, Callable, Coroutine, Optional
1817

1918
import aiohttp
20-
from Crypto.Cipher import AES
21-
from Crypto.Util.Padding import unpad
2219

2320
from .code_mappings import RoborockDockTypeCode
2421
from .containers import (
@@ -46,6 +43,7 @@
4643
RoborockUrlException,
4744
VacuumError,
4845
)
46+
from .protocol import Utils
4947
from .roborock_future import RoborockFuture
5048
from .roborock_message import RoborockMessage
5149
from .roborock_typing import DeviceProp, DockSummary, RoborockCommand
@@ -148,15 +146,13 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None:
148146
payload = data.payload[0:24]
149147
[endpoint, _, request_id, _] = struct.unpack("<15sBH6s", payload)
150148
if endpoint.decode().startswith(self._endpoint):
151-
iv = bytes(AES.block_size)
152-
decipher = AES.new(self._nonce, AES.MODE_CBC, iv)
153-
decrypted = unpad(decipher.decrypt(data.payload[24:]), AES.block_size)
154-
decrypted = gzip.decompress(decrypted)
149+
decrypted = Utils.decrypt_cbc(data.payload[24:], self._nonce)
150+
decompressed = Utils.decompress(decrypted)
155151
queue = self._waiting_queue.get(request_id)
156152
if queue:
157-
if isinstance(decrypted, list):
158-
decrypted = decrypted[0]
159-
queue.resolve((decrypted, None))
153+
if isinstance(decompressed, list):
154+
decompressed = decompressed[0]
155+
queue.resolve((decompressed, None))
160156
else:
161157
queue = self._waiting_queue.get(data.seq)
162158
if queue:

roborock/cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,18 @@ async def command(ctx, cmd, device_id, params):
123123
home_data = login_data.home_data
124124
devices = home_data.devices + home_data.received_devices
125125
device = next((device for device in devices if device.duid == device_id), None)
126+
if device is None:
127+
raise RoborockException("No device found")
126128
model_specification = next(
127-
(product.model_specification for product in home_data.products if product.did == device.duid), None
129+
(
130+
product.model_specification
131+
for product in home_data.products
132+
if device is not None and product.did == device.duid
133+
),
134+
None,
128135
)
136+
if model_specification is None:
137+
raise RoborockException(f"Could not find model specifications for device {device.name}")
129138
device_info = RoborockDeviceInfo(device=device, model_specification=model_specification)
130139
mqtt_client = RoborockMqttClient(login_data.user_data, device_info)
131140
await mqtt_client.send_command(cmd, params)

roborock/cloud_api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
from .api import COMMANDS_SECURED, KEEPALIVE, RoborockClient, md5hex
1414
from .containers import RoborockDeviceInfo, UserData
1515
from .exceptions import CommandVacuumError, RoborockException, VacuumError
16-
from .protocol import Utils
16+
from .protocol import MessageParser, Utils
1717
from .roborock_future import RoborockFuture
18-
from .roborock_message import RoborockMessage, RoborockParser
18+
from .roborock_message import RoborockMessage
1919
from .roborock_typing import RoborockCommand
2020

2121
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +77,7 @@ def on_connect(self, *args, **kwargs):
7777
def on_message(self, *args, **kwargs):
7878
_, __, msg = args
7979
try:
80-
messages, _ = RoborockParser.decode(msg.payload, self.device_info.device.local_key)
80+
messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key)
8181
super().on_message_received(messages)
8282
except Exception as ex:
8383
_LOGGER.exception(ex)
@@ -156,7 +156,7 @@ async def send_command(self, method: RoborockCommand, params: Optional[list | di
156156
response_protocol = 301 if method in COMMANDS_SECURED else 102
157157
roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload)
158158
local_key = self.device_info.device.local_key
159-
msg = RoborockParser.encode(roborock_message, local_key)
159+
msg = MessageParser.build(roborock_message, local_key)
160160
self._send_msg_raw(msg)
161161
(response, err) = await self._async_response(request_id, response_protocol)
162162
if err:

roborock/containers.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,6 @@ class RoborockDeviceInfo(RoborockBase):
388388
model_specification: ModelSpecification
389389

390390

391-
@dataclass
392-
class RoborockLocalDeviceInfo(RoborockDeviceInfo):
393-
network_info: NetworkInfo
394-
395-
396391
@dataclass
397392
class RoomMapping(RoborockBase):
398393
segment_id: int

roborock/local_api.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@
88
import async_timeout
99

1010
from .api import COMMANDS_SECURED, QUEUE_TIMEOUT, RoborockClient
11-
from .containers import RoborockLocalDeviceInfo
11+
from .containers import RoborockDeviceInfo
1212
from .exceptions import CommandVacuumError, RoborockConnectionException, RoborockException
13-
from .roborock_message import AP_CONFIG, RoborockMessage, RoborockParser
13+
from .protocol import AP_CONFIG, MessageParser
14+
from .roborock_message import RoborockMessage
1415
from .roborock_typing import CommandInfoMap, RoborockCommand
1516
from .util import get_running_loop_or_create_one
1617

1718
_LOGGER = logging.getLogger(__name__)
1819

1920

2021
class RoborockLocalClient(RoborockClient, asyncio.Protocol):
21-
def __init__(self, device_info: RoborockLocalDeviceInfo):
22+
def __init__(self, device_info: RoborockDeviceInfo, ip: str):
2223
super().__init__("abc", device_info)
2324
self.loop = get_running_loop_or_create_one()
24-
self.ip = device_info.network_info.ip
25+
self.ip = ip
2526
self._batch_structs: list[RoborockMessage] = []
2627
self._executing = False
2728
self.remaining = b""
@@ -32,8 +33,7 @@ def data_received(self, message):
3233
if self.remaining:
3334
message = self.remaining + message
3435
self.remaining = b""
35-
(parser_msg, remaining) = RoborockParser.decode(message, self.device_info.device.local_key)
36-
self.remaining = remaining
36+
parser_msg, self.remaining = MessageParser.parse(message, local_key=self.device_info.device.local_key)
3737
self.on_message_received(parser_msg)
3838

3939
def connection_lost(self, exc: Optional[Exception]):
@@ -92,11 +92,16 @@ async def send_command(self, method: RoborockCommand, params: Optional[list | di
9292
return (await self.send_message(roborock_message))[0]
9393

9494
async def async_local_response(self, roborock_message: RoborockMessage):
95-
request_id = roborock_message.get_request_id()
96-
if request_id is None:
95+
method = roborock_message.get_method()
96+
request_id: int | None
97+
if method and not method.startswith("get"):
9798
request_id = roborock_message.seq
98-
# response_protocol = 5 if roborock_message.prefix == secured_prefix else 4
99-
response_protocol = 4
99+
response_protocol = 5
100+
else:
101+
request_id = roborock_message.get_request_id()
102+
response_protocol = 4
103+
if request_id is None:
104+
raise RoborockException(f"Failed build message {roborock_message}")
100105
(response, err) = await self._async_response(request_id, response_protocol)
101106
if err:
102107
raise CommandVacuumError("", err) from err
@@ -116,7 +121,7 @@ async def send_message(self, roborock_messages: list[RoborockMessage] | Roborock
116121
if isinstance(roborock_messages, RoborockMessage):
117122
roborock_messages = [roborock_messages]
118123
local_key = self.device_info.device.local_key
119-
msg = RoborockParser.encode(roborock_messages, local_key)
124+
msg = MessageParser.build(roborock_messages, local_key=local_key)
120125
# Send the command to the Roborock device
121126
_LOGGER.debug(f"Requesting device with {roborock_messages}")
122127
self._send_msg_raw(msg)

0 commit comments

Comments
 (0)