From bc96ab303d513413839470f4d4f9a170928ede34 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:24:55 -0400 Subject: [PATCH 1/8] major: add A01 --- commitlint.config.js | 13 +++ pyproject.toml | 9 ++ roborock/version_a01_apis/__init__.py | 0 .../version_a01_apis/roborock_client_a01.py | 98 +++++++++++++++++++ .../roborock_mqtt_client_a01.py | 55 +++++++++++ 5 files changed, 175 insertions(+) create mode 100644 roborock/version_a01_apis/__init__.py create mode 100644 roborock/version_a01_apis/roborock_client_a01.py create mode 100644 roborock/version_a01_apis/roborock_mqtt_client_a01.py diff --git a/commitlint.config.js b/commitlint.config.js index 25e11892..3a23220e 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,4 +1,17 @@ module.exports = { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], + rules: { + 'type-enum': [ + RuleConfigSeverity.Error, + 'always', + [ + 'chore', + 'docs', + 'feat', + 'fix', + 'major' + ], + + } }; diff --git a/pyproject.toml b/pyproject.toml index eeda2c88..fd0e159f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,15 @@ pyshark = "^0.6" branch = "main" version_toml = "pyproject.toml:tool.poetry.version" build_command = "pip install poetry && poetry build" +[tool.semantic_release.commit_parser_options] +allowed_tags = [ + "chore", + "docs", + "feat", + "fix", + "major" +] +major_tags= ["major"] [tool.ruff] ignore = ["F403", "E741"] diff --git a/roborock/version_a01_apis/__init__.py b/roborock/version_a01_apis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py new file mode 100644 index 00000000..0711e079 --- /dev/null +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -0,0 +1,98 @@ +import dataclasses +import json +import typing +from collections.abc import Callable +from datetime import time + +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad + +from roborock import DeviceData +from roborock.api import RoborockClient +from roborock.code_mappings import ( + DyadBrushSpeed, + DyadCleanMode, + DyadError, + DyadSelfCleanLevel, + DyadSelfCleanMode, + DyadSuction, + DyadWarmLevel, + DyadWaterLevel, + RoborockDyadStateCode, +) +from roborock.containers import DyadProductInfo, DyadSndState +from roborock.roborock_message import ( + RoborockDyadDataProtocol, + RoborockMessage, + RoborockMessageProtocol, +) + + +@dataclasses.dataclass +class DyadProtocolCacheEntry: + post_process_fn: Callable + value: typing.Any | None = None + + +# Right now this cache is not active, it was too much complexity for the initial addition of dyad. +protocol_entries = { + RoborockDyadDataProtocol.STATUS: DyadProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name), + RoborockDyadDataProtocol.SELF_CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name), + RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: DyadProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name), + RoborockDyadDataProtocol.WARM_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWarmLevel(val).name), + RoborockDyadDataProtocol.CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadCleanMode(val).name), + RoborockDyadDataProtocol.SUCTION: DyadProtocolCacheEntry(lambda val: DyadSuction(val).name), + RoborockDyadDataProtocol.WATER_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWaterLevel(val).name), + RoborockDyadDataProtocol.BRUSH_SPEED: DyadProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name), + RoborockDyadDataProtocol.POWER: DyadProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.AUTO_DRY: DyadProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.MESH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)), + RoborockDyadDataProtocol.BRUSH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)), + RoborockDyadDataProtocol.ERROR: DyadProtocolCacheEntry(lambda val: DyadError(val).name), + RoborockDyadDataProtocol.VOLUME_SET: DyadProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: DyadProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.AUTO_DRY_MODE: DyadProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.SILENT_DRY_DURATION: DyadProtocolCacheEntry(lambda val: int(val)), # in minutes + RoborockDyadDataProtocol.SILENT_MODE: DyadProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: DyadProtocolCacheEntry( + lambda val: time(hour=int(val / 60), minute=val % 60) + ), # in minutes since 00:00 + RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry( + lambda val: time(hour=int(val / 60), minute=val % 60) + ), # in minutes since 00:00 + RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry( + lambda val: [int(v) for v in val.split(",")] + ), # minutes of cleaning in past few days. + RoborockDyadDataProtocol.TOTAL_RUN_TIME: DyadProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.SND_STATE: DyadProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)), + RoborockDyadDataProtocol.PRODUCT_INFO: DyadProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)), +} + + +class RoborockClientA01(RoborockClient): + def __init__(self, endpoint: str, device_info: DeviceData): + super().__init__(endpoint, device_info) + + def on_message_received(self, messages: list[RoborockMessage]) -> None: + for message in messages: + protocol = message.protocol + if message.payload and protocol in [ + RoborockMessageProtocol.RPC_RESPONSE, + RoborockMessageProtocol.GENERAL_REQUEST, + ]: + payload = message.payload + try: + payload = unpad(payload, AES.block_size) + except Exception: + continue + payload_json = json.loads(payload.decode()) + for data_point_number, data_point in payload_json.get("dps").items(): + data_point_protocol = RoborockDyadDataProtocol(int(data_point_number)) + if data_point_protocol in protocol_entries: + converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point) + queue = self._waiting_queue.get(int(data_point_number)) + if queue and queue.protocol == protocol: + queue.resolve((converted_response, None)) + + async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]): + raise NotImplementedError diff --git a/roborock/version_a01_apis/roborock_mqtt_client_a01.py b/roborock/version_a01_apis/roborock_mqtt_client_a01.py new file mode 100644 index 00000000..7115d914 --- /dev/null +++ b/roborock/version_a01_apis/roborock_mqtt_client_a01.py @@ -0,0 +1,55 @@ +import asyncio +import base64 +import json + +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + +from roborock.cloud_api import RoborockMqttClient +from roborock.containers import DeviceData, UserData +from roborock.exceptions import RoborockException +from roborock.protocol import MessageParser, Utils +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol + +from .roborock_client_a01 import RoborockClientA01 + + +class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01): + def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None: + rriot = user_data.rriot + if rriot is None: + raise RoborockException("Got no rriot data from user_data") + endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode() + + RoborockMqttClient.__init__(self, user_data, device_info, queue_timeout) + RoborockClientA01.__init__(self, endpoint, device_info) + + async def send_message(self, roborock_message: RoborockMessage): + await self.validate_connection() + response_protocol = RoborockMessageProtocol.RPC_RESPONSE + + local_key = self.device_info.device.local_key + m = MessageParser.build(roborock_message, local_key, prefixed=False) + # self._logger.debug(f"id={request_id} Requesting method {method} with {params}") + payload = json.loads(unpad(roborock_message.payload, AES.block_size)) + futures = [] + if "10000" in payload["dps"]: + for dps in json.loads(payload["dps"]["10000"]): + futures.append(asyncio.ensure_future(self._async_response(dps, response_protocol))) + self._send_msg_raw(m) + responses = await asyncio.gather(*futures) + dps_responses = {} + if "10000" in payload["dps"]: + for i, dps in enumerate(json.loads(payload["dps"]["10000"])): + dps_responses[dps] = responses[i][0] + return dps_responses + + async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]): + payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}} + return await self.send_message( + RoborockMessage( + protocol=RoborockMessageProtocol.RPC_REQUEST, + version=b"A01", + payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size), + ) + ) From 0c2a9864405967f59751bb0fc0e1775e1350c486 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:32:27 -0400 Subject: [PATCH 2/8] chore: add init --- pyproject.toml | 3 +++ roborock/version_1_apis/__init__.py | 3 +++ roborock/version_a01_apis/__init__.py | 2 ++ 3 files changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fd0e159f..f76e3ea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,3 +63,6 @@ major_tags= ["major"] ignore = ["F403", "E741"] line-length = 120 select=["E", "F", "UP", "I"] + +[tool.ruff.lint.per-file-ignores] +"*/__init__.py" = ["F401"] diff --git a/roborock/version_1_apis/__init__.py b/roborock/version_1_apis/__init__.py index e69de29b..61651d49 100644 --- a/roborock/version_1_apis/__init__.py +++ b/roborock/version_1_apis/__init__.py @@ -0,0 +1,3 @@ +from .roborock_client_v1 import AttributeCache, RoborockClientV1 +from .roborock_local_client_v1 import RoborockLocalClientV1 +from .roborock_mqtt_client_v1 import RoborockMqttClientV1 diff --git a/roborock/version_a01_apis/__init__.py b/roborock/version_a01_apis/__init__.py index e69de29b..0cf0765d 100644 --- a/roborock/version_a01_apis/__init__.py +++ b/roborock/version_a01_apis/__init__.py @@ -0,0 +1,2 @@ +from .roborock_client_a01 import RoborockClientA01 +from .roborock_mqtt_client_a01 import RoborockMqttClientA01 From 761863c724963a2d0e93c896f4e39e13c88cf470 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:36:30 -0400 Subject: [PATCH 3/8] chore: fix commitlint? --- commitlint.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 3a23220e..a687b23a 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -2,7 +2,7 @@ module.exports = { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], rules: { - 'type-enum': [ + 'type-enum': [ RuleConfigSeverity.Error, 'always', [ @@ -12,6 +12,6 @@ module.exports = { 'fix', 'major' ], - + ] } }; From f61c5024c4cef6f90ea31fec6e9cff9650b55444 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:41:47 -0400 Subject: [PATCH 4/8] chore: fix commitlint --- commitlint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commitlint.config.js b/commitlint.config.js index a687b23a..0e6bd784 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,7 @@ +import { + RuleConfigSeverity, +} from '@commitlint/types'; + module.exports = { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], From eb008f06c3e93358cd3587ae6a98b6fc21e9afda Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:46:01 -0400 Subject: [PATCH 5/8] chore: fix commitlint --- commitlint.config.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 0e6bd784..ca0adcef 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,13 +1,9 @@ -import { - RuleConfigSeverity, -} from '@commitlint/types'; - module.exports = { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], rules: { 'type-enum': [ - RuleConfigSeverity.Error, + 2, 'always', [ 'chore', From 6be34e5aeb4279fb52ac274ef6509aa8cbb0c927 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:56:06 -0400 Subject: [PATCH 6/8] chore: change refactor to be major tag --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f76e3ea8..e0e25bd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,9 +55,9 @@ allowed_tags = [ "docs", "feat", "fix", - "major" + "refactor" ] -major_tags= ["major"] +major_tags= ["refactor"] [tool.ruff] ignore = ["F403", "E741"] From bc3a9bf4e58d9c5f2601c496ea343e337f6213ec Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 12:01:23 -0400 Subject: [PATCH 7/8] refactor: add A01 --- commitlint.config.js | 13 ------------- roborock/version_a01_apis/roborock_client_a01.py | 1 + 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index ca0adcef..25e11892 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,17 +1,4 @@ module.exports = { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], - rules: { - 'type-enum': [ - 2, - 'always', - [ - 'chore', - 'docs', - 'feat', - 'fix', - 'major' - ], - ] - } }; diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 0711e079..f812c1ea 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -89,6 +89,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: for data_point_number, data_point in payload_json.get("dps").items(): data_point_protocol = RoborockDyadDataProtocol(int(data_point_number)) if data_point_protocol in protocol_entries: + # Auto convert into data struct we want. converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point) queue = self._waiting_queue.get(int(data_point_number)) if queue and queue.protocol == protocol: From f7947fce89b241ea4b967fe31696f8f6a88c0d41 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 15:49:07 -0400 Subject: [PATCH 8/8] feat: add a01 BREAKING CHANGE: You must now specify what version api you want to use with clients. --- roborock/version_a01_apis/roborock_client_a01.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index f812c1ea..4127935c 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -96,4 +96,5 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: queue.resolve((converted_response, None)) async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]): + """This should handle updating for each given protocol.""" raise NotImplementedError