Skip to content

Commit 16b9e3e

Browse files
authored
refactor: add A01 (#199)
* major: add A01 * chore: add init * chore: fix commitlint? * chore: fix commitlint * chore: fix commitlint * chore: change refactor to be major tag * refactor: add A01 * feat: add a01 BREAKING CHANGE: You must now specify what version api you want to use with clients.
1 parent 30d2577 commit 16b9e3e

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-0
lines changed

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,20 @@ pyshark = "^0.6"
4949
branch = "main"
5050
version_toml = "pyproject.toml:tool.poetry.version"
5151
build_command = "pip install poetry && poetry build"
52+
[tool.semantic_release.commit_parser_options]
53+
allowed_tags = [
54+
"chore",
55+
"docs",
56+
"feat",
57+
"fix",
58+
"refactor"
59+
]
60+
major_tags= ["refactor"]
5261

5362
[tool.ruff]
5463
ignore = ["F403", "E741"]
5564
line-length = 120
5665
select=["E", "F", "UP", "I"]
66+
67+
[tool.ruff.lint.per-file-ignores]
68+
"*/__init__.py" = ["F401"]

roborock/version_1_apis/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .roborock_client_v1 import AttributeCache, RoborockClientV1
2+
from .roborock_local_client_v1 import RoborockLocalClientV1
3+
from .roborock_mqtt_client_v1 import RoborockMqttClientV1

roborock/version_a01_apis/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .roborock_client_a01 import RoborockClientA01
2+
from .roborock_mqtt_client_a01 import RoborockMqttClientA01
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import dataclasses
2+
import json
3+
import typing
4+
from collections.abc import Callable
5+
from datetime import time
6+
7+
from Crypto.Cipher import AES
8+
from Crypto.Util.Padding import unpad
9+
10+
from roborock import DeviceData
11+
from roborock.api import RoborockClient
12+
from roborock.code_mappings import (
13+
DyadBrushSpeed,
14+
DyadCleanMode,
15+
DyadError,
16+
DyadSelfCleanLevel,
17+
DyadSelfCleanMode,
18+
DyadSuction,
19+
DyadWarmLevel,
20+
DyadWaterLevel,
21+
RoborockDyadStateCode,
22+
)
23+
from roborock.containers import DyadProductInfo, DyadSndState
24+
from roborock.roborock_message import (
25+
RoborockDyadDataProtocol,
26+
RoborockMessage,
27+
RoborockMessageProtocol,
28+
)
29+
30+
31+
@dataclasses.dataclass
32+
class DyadProtocolCacheEntry:
33+
post_process_fn: Callable
34+
value: typing.Any | None = None
35+
36+
37+
# Right now this cache is not active, it was too much complexity for the initial addition of dyad.
38+
protocol_entries = {
39+
RoborockDyadDataProtocol.STATUS: DyadProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
40+
RoborockDyadDataProtocol.SELF_CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
41+
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: DyadProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
42+
RoborockDyadDataProtocol.WARM_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
43+
RoborockDyadDataProtocol.CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
44+
RoborockDyadDataProtocol.SUCTION: DyadProtocolCacheEntry(lambda val: DyadSuction(val).name),
45+
RoborockDyadDataProtocol.WATER_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
46+
RoborockDyadDataProtocol.BRUSH_SPEED: DyadProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
47+
RoborockDyadDataProtocol.POWER: DyadProtocolCacheEntry(lambda val: int(val)),
48+
RoborockDyadDataProtocol.AUTO_DRY: DyadProtocolCacheEntry(lambda val: bool(val)),
49+
RoborockDyadDataProtocol.MESH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
50+
RoborockDyadDataProtocol.BRUSH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
51+
RoborockDyadDataProtocol.ERROR: DyadProtocolCacheEntry(lambda val: DyadError(val).name),
52+
RoborockDyadDataProtocol.VOLUME_SET: DyadProtocolCacheEntry(lambda val: int(val)),
53+
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: DyadProtocolCacheEntry(lambda val: bool(val)),
54+
RoborockDyadDataProtocol.AUTO_DRY_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
55+
RoborockDyadDataProtocol.SILENT_DRY_DURATION: DyadProtocolCacheEntry(lambda val: int(val)), # in minutes
56+
RoborockDyadDataProtocol.SILENT_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
57+
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: DyadProtocolCacheEntry(
58+
lambda val: time(hour=int(val / 60), minute=val % 60)
59+
), # in minutes since 00:00
60+
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry(
61+
lambda val: time(hour=int(val / 60), minute=val % 60)
62+
), # in minutes since 00:00
63+
RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry(
64+
lambda val: [int(v) for v in val.split(",")]
65+
), # minutes of cleaning in past few days.
66+
RoborockDyadDataProtocol.TOTAL_RUN_TIME: DyadProtocolCacheEntry(lambda val: int(val)),
67+
RoborockDyadDataProtocol.SND_STATE: DyadProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
68+
RoborockDyadDataProtocol.PRODUCT_INFO: DyadProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
69+
}
70+
71+
72+
class RoborockClientA01(RoborockClient):
73+
def __init__(self, endpoint: str, device_info: DeviceData):
74+
super().__init__(endpoint, device_info)
75+
76+
def on_message_received(self, messages: list[RoborockMessage]) -> None:
77+
for message in messages:
78+
protocol = message.protocol
79+
if message.payload and protocol in [
80+
RoborockMessageProtocol.RPC_RESPONSE,
81+
RoborockMessageProtocol.GENERAL_REQUEST,
82+
]:
83+
payload = message.payload
84+
try:
85+
payload = unpad(payload, AES.block_size)
86+
except Exception:
87+
continue
88+
payload_json = json.loads(payload.decode())
89+
for data_point_number, data_point in payload_json.get("dps").items():
90+
data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
91+
if data_point_protocol in protocol_entries:
92+
# Auto convert into data struct we want.
93+
converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point)
94+
queue = self._waiting_queue.get(int(data_point_number))
95+
if queue and queue.protocol == protocol:
96+
queue.resolve((converted_response, None))
97+
98+
async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
99+
"""This should handle updating for each given protocol."""
100+
raise NotImplementedError
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import asyncio
2+
import base64
3+
import json
4+
5+
from Crypto.Cipher import AES
6+
from Crypto.Util.Padding import pad, unpad
7+
8+
from roborock.cloud_api import RoborockMqttClient
9+
from roborock.containers import DeviceData, UserData
10+
from roborock.exceptions import RoborockException
11+
from roborock.protocol import MessageParser, Utils
12+
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol
13+
14+
from .roborock_client_a01 import RoborockClientA01
15+
16+
17+
class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
18+
def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None:
19+
rriot = user_data.rriot
20+
if rriot is None:
21+
raise RoborockException("Got no rriot data from user_data")
22+
endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode()
23+
24+
RoborockMqttClient.__init__(self, user_data, device_info, queue_timeout)
25+
RoborockClientA01.__init__(self, endpoint, device_info)
26+
27+
async def send_message(self, roborock_message: RoborockMessage):
28+
await self.validate_connection()
29+
response_protocol = RoborockMessageProtocol.RPC_RESPONSE
30+
31+
local_key = self.device_info.device.local_key
32+
m = MessageParser.build(roborock_message, local_key, prefixed=False)
33+
# self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
34+
payload = json.loads(unpad(roborock_message.payload, AES.block_size))
35+
futures = []
36+
if "10000" in payload["dps"]:
37+
for dps in json.loads(payload["dps"]["10000"]):
38+
futures.append(asyncio.ensure_future(self._async_response(dps, response_protocol)))
39+
self._send_msg_raw(m)
40+
responses = await asyncio.gather(*futures)
41+
dps_responses = {}
42+
if "10000" in payload["dps"]:
43+
for i, dps in enumerate(json.loads(payload["dps"]["10000"])):
44+
dps_responses[dps] = responses[i][0]
45+
return dps_responses
46+
47+
async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
48+
payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}}
49+
return await self.send_message(
50+
RoborockMessage(
51+
protocol=RoborockMessageProtocol.RPC_REQUEST,
52+
version=b"A01",
53+
payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
54+
)
55+
)

0 commit comments

Comments
 (0)