From bc96ab303d513413839470f4d4f9a170928ede34 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 8 Apr 2024 11:24:55 -0400 Subject: [PATCH 01/13] 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 25e1189..3a23220 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 eeda2c8..fd0e159 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 0000000..e69de29 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 0000000..0711e07 --- /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 0000000..7115d91 --- /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 02/13] 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 fd0e159..f76e3ea 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 e69de29..61651d4 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 e69de29..0cf0765 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 03/13] 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 3a23220..a687b23 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 04/13] chore: fix commitlint --- commitlint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commitlint.config.js b/commitlint.config.js index a687b23..0e6bd78 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 05/13] 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 0e6bd78..ca0adce 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 06/13] 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 f76e3ea..e0e25bd 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 07/13] 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 ca0adce..25e1189 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 0711e07..f812c1e 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 08/13] 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 f812c1e..4127935 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 From 651b6997b5b3902e9552c50173ae84cadd86bb0d Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 10 Apr 2024 09:32:02 -0400 Subject: [PATCH 09/13] feat: add initial zeo support --- roborock/code_mappings.py | 57 +++++++++++++ roborock/roborock_message.py | 11 +++ .../version_a01_apis/roborock_client_a01.py | 82 ++++++++++++------- .../roborock_mqtt_client_a01.py | 8 +- 4 files changed, 125 insertions(+), 33 deletions(-) diff --git a/roborock/code_mappings.py b/roborock/code_mappings.py index 244314d..dd9a8aa 100644 --- a/roborock/code_mappings.py +++ b/roborock/code_mappings.py @@ -450,3 +450,60 @@ class DyadError(RoborockEnum): dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts. low_battery = 20017 # Low battery level. Charge before starting self-cleaning. battery_under_10 = 20018 # Charge until the battery level exceeds 10% before manually starting self-cleaning. + + +class ZeoMode(RoborockEnum): + wash = 1 + wash_and_dry = 2 + dry = 3 + + +class ZeoState(RoborockEnum): + standby = 1 + weighing = 2 + soaking = 3 + washing = 4 + rinsing = 5 + spinning = 6 + drying = 7 + cooling = 8 + under_delay_start = 9 + done = 10 + + +class ZeoProgram(RoborockEnum): + standard = 1 + quick = 2 + sanitize = 3 + wool = 4 + air_refresh = 5 + custom = 6 + bedding = 7 + down = 8 + silk = 9 + rinse_and_spin = 10 + spin = 11 + down_clean = 12 + baby_care = 13 + anti_allergen = 14 + sportswear = 15 + night = 16 + new_clothes = 17 + shirts = 18 + synthetics = 19 + underwear = 20 + gentle = 21 + intensive = 22 + cotton_linen = 23 + season = 24 + warming = 25 + bra = 26 + panties = 27 + boiling_wash = 28 + socks = 30 + towels = 31 + anti_mite = 32 + exo_40_60 = 33 + twenty_c = 34 + t_shirts = 35 + stain_removal = 36 diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index 2128892..afb4d2c 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -87,6 +87,17 @@ class RoborockDyadDataProtocol(RoborockEnum): RPC_RESPONSE = 10102 +class RoborockZeoProtocol(RoborockEnum): + MODE = 204 + PAUSE = 201 + STATE = 203 + OTA_NFO = 10007 + PROGRAM = 205 + SHUTDOWN = 202 + COUNTDOWN = 217 + WASHING_LEFT = 218 + + ROBOROCK_DATA_STATUS_PROTOCOL = [ RoborockDataProtocol.ERROR_CODE, RoborockDataProtocol.STATE, diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 4127935..4f7977b 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -19,59 +19,72 @@ DyadWarmLevel, DyadWaterLevel, RoborockDyadStateCode, + ZeoMode, + ZeoProgram, + ZeoState, ) -from roborock.containers import DyadProductInfo, DyadSndState +from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory from roborock.roborock_message import ( RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol, + RoborockZeoProtocol, ) @dataclasses.dataclass -class DyadProtocolCacheEntry: +class A01ProtocolCacheEntry: 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( + RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name), + RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name), + RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name), + RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name), + RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name), + RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name), + RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name), + RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name), + RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), + RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), + RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name), + RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes + RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry( lambda val: time(hour=int(val / 60), minute=val % 60) ), # in minutes since 00:00 - RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry( + RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry( lambda val: time(hour=int(val / 60), minute=val % 60) ), # in minutes since 00:00 - RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry( + RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry( 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)), + RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)), + RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)), +} + +zeo_data_protocol_entries = { + RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name), + RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name), + RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val)), + RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: ZeoProgram(val)), } class RoborockClientA01(RoborockClient): - def __init__(self, endpoint: str, device_info: DeviceData): + def __init__(self, endpoint: str, device_info: DeviceData, category: RoborockCategory): super().__init__(endpoint, device_info) + self.category = category def on_message_received(self, messages: list[RoborockMessage]) -> None: for message in messages: @@ -87,10 +100,19 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: 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: + data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol + entries: dict + if self.category == RoborockCategory.WET_DRY_VAC: + data_point_protocol = RoborockDyadDataProtocol(int(data_point_number)) + entries = protocol_entries + elif self.category == RoborockCategory.WASHING_MACHINE: + data_point_protocol = RoborockZeoProtocol(int(data_point_number)) + entries = zeo_data_protocol_entries + else: + continue + if data_point_protocol in entries: # Auto convert into data struct we want. - converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point) + converted_response = 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)) diff --git a/roborock/version_a01_apis/roborock_mqtt_client_a01.py b/roborock/version_a01_apis/roborock_mqtt_client_a01.py index 7115d91..ef68f5e 100644 --- a/roborock/version_a01_apis/roborock_mqtt_client_a01.py +++ b/roborock/version_a01_apis/roborock_mqtt_client_a01.py @@ -6,7 +6,7 @@ from Crypto.Util.Padding import pad, unpad from roborock.cloud_api import RoborockMqttClient -from roborock.containers import DeviceData, UserData +from roborock.containers import DeviceData, RoborockCategory, UserData from roborock.exceptions import RoborockException from roborock.protocol import MessageParser, Utils from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol @@ -15,14 +15,16 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01): - def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None: + def __init__( + self, user_data: UserData, device_info: DeviceData, category: RoborockCategory, 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) + RoborockClientA01.__init__(self, endpoint, device_info, category) async def send_message(self, roborock_message: RoborockMessage): await self.validate_connection() From 76ef7d9b45939eabdd99ccf63fba228701838057 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 11 Apr 2024 08:34:35 -0400 Subject: [PATCH 10/13] fix: fix A01 support --- roborock/api.py | 9 ----- roborock/protocol.py | 5 +-- roborock/roborock_message.py | 38 +++++++++++++++++-- .../version_a01_apis/roborock_client_a01.py | 2 +- .../roborock_mqtt_client_a01.py | 9 ++++- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index 7a4338f..d6da5eb 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -12,9 +12,6 @@ from .containers import ( DeviceData, - ModelStatus, - S7MaxVStatus, - Status, ) from .exceptions import ( RoborockTimeout, @@ -48,16 +45,10 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER) self.is_available: bool = True self.queue_timeout = queue_timeout - self._status_type: type[Status] = ModelStatus.get(self.device_info.model, S7MaxVStatus) def __del__(self) -> None: self.release() - @property - def status_type(self) -> type[Status]: - """Gets the status type for this device""" - return self._status_type - def release(self): self.sync_disconnect() diff --git a/roborock/protocol.py b/roborock/protocol.py index 603f06c..a5f3576 100644 --- a/roborock/protocol.py +++ b/roborock/protocol.py @@ -36,7 +36,6 @@ _LOGGER = logging.getLogger(__name__) SALT = b"TXdfu$jyZ#TZHsg4" A01_HASH = "726f626f726f636b2d67a6d6da" -A01_AES_DECIPHER = "ELSYN0wTI4AUm7C4" BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe" AP_CONFIG = 1 SOCK_DISCOVERY = 2 @@ -208,7 +207,7 @@ def _encode(self, obj, context, _): """ if context.version == b"A01": iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24] - decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) + decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) f = decipher.encrypt(obj) return f token = self.token_func(context) @@ -219,7 +218,7 @@ def _decode(self, obj, context, _): """Decrypts the given payload with the token stored in the context.""" if context.version == b"A01": iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24] - decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) + decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) f = decipher.decrypt(obj) return f token = self.token_func(context) diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index afb4d2c..230a1d5 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -88,14 +88,46 @@ class RoborockDyadDataProtocol(RoborockEnum): class RoborockZeoProtocol(RoborockEnum): - MODE = 204 + DRYING_STATUS = 134 + START = 200 PAUSE = 201 + SHUTDOWN = 202 STATE = 203 - OTA_NFO = 10007 + MODE = 204 PROGRAM = 205 - SHUTDOWN = 202 + CHILD_LOCK = 206 + TEMP = 207 + RINSE_TIMES = 208 + SPIN_LEVEL = 209 + DRYING_MODE = 210 + DETERGENT_SET = 211 + SOFTENER_SET = 212 + DETERGENT_TYPE = 213 + SOFTENER_TYPE = 214 COUNTDOWN = 217 WASHING_LEFT = 218 + DOORLOCK_STATE = 219 + ERROR = 220 + CUSTOM_PARAM_SAVE = 221 + CUSTOM_PARAM_GET = 222 + SOUND_SET = 223 + TIMES_AFTER_CLEAN = 224 + DEFAULT_SETTING = 225 + DETERGENT_EMPTY = 226 + SOFTENER_EMPTY = 227 + LIGHT_SETTING = 229 + DETERGENT_VOLUME = 230 + SOFTENER_VOLUME = 231 + APP_AUTHORIZATION = 232 + ID_QUERY = 10000 + F_C = 10001 + SND_STATE = 10004 + PRODUCT_INFO = 10005 + PRIVACY_INFO = 10006 + OTA_NFO = 10007 + WASHING_LOG = 10008 + RPC_REQ = 10101 + RPC_RESp = 10102 ROBOROCK_DATA_STATUS_PROTOCOL = [ diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 4f7977b..8b277e7 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -117,6 +117,6 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: if queue and queue.protocol == protocol: queue.resolve((converted_response, None)) - async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]): + async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]): """This should handle updating for each given protocol.""" raise NotImplementedError diff --git a/roborock/version_a01_apis/roborock_mqtt_client_a01.py b/roborock/version_a01_apis/roborock_mqtt_client_a01.py index ef68f5e..4258069 100644 --- a/roborock/version_a01_apis/roborock_mqtt_client_a01.py +++ b/roborock/version_a01_apis/roborock_mqtt_client_a01.py @@ -9,7 +9,12 @@ from roborock.containers import DeviceData, RoborockCategory, UserData from roborock.exceptions import RoborockException from roborock.protocol import MessageParser, Utils -from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol +from roborock.roborock_message import ( + RoborockDyadDataProtocol, + RoborockMessage, + RoborockMessageProtocol, + RoborockZeoProtocol, +) from .roborock_client_a01 import RoborockClientA01 @@ -46,7 +51,7 @@ async def send_message(self, roborock_message: RoborockMessage): dps_responses[dps] = responses[i][0] return dps_responses - async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]): + async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]): payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}} return await self.send_message( RoborockMessage( From 68992baab9fef84f2b570c3a4c617a9909f0422a Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 11 Apr 2024 08:52:36 -0400 Subject: [PATCH 11/13] fix: allow messages to fail --- roborock/version_a01_apis/roborock_client_a01.py | 4 ++-- .../version_a01_apis/roborock_mqtt_client_a01.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 8b277e7..5b42344 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -82,8 +82,8 @@ class A01ProtocolCacheEntry: class RoborockClientA01(RoborockClient): - def __init__(self, endpoint: str, device_info: DeviceData, category: RoborockCategory): - super().__init__(endpoint, device_info) + def __init__(self, endpoint: str, device_info: DeviceData, category: RoborockCategory, queue_timeout: int = 4): + super().__init__(endpoint, device_info, queue_timeout) self.category = category def on_message_received(self, messages: list[RoborockMessage]) -> None: diff --git a/roborock/version_a01_apis/roborock_mqtt_client_a01.py b/roborock/version_a01_apis/roborock_mqtt_client_a01.py index 4258069..8d4fb2a 100644 --- a/roborock/version_a01_apis/roborock_mqtt_client_a01.py +++ b/roborock/version_a01_apis/roborock_mqtt_client_a01.py @@ -1,6 +1,7 @@ import asyncio import base64 import json +import typing from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad @@ -29,7 +30,7 @@ def __init__( 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, category) + RoborockClientA01.__init__(self, endpoint, device_info, category, queue_timeout) async def send_message(self, roborock_message: RoborockMessage): await self.validate_connection() @@ -44,11 +45,16 @@ async def send_message(self, roborock_message: RoborockMessage): 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 = {} + responses = await asyncio.gather(*futures, return_exceptions=True) + dps_responses: dict[int, typing.Any] = {} if "10000" in payload["dps"]: for i, dps in enumerate(json.loads(payload["dps"]["10000"])): - dps_responses[dps] = responses[i][0] + response = responses[i] + if isinstance(response, BaseException): + self._logger.warning("Timed out get req for %s after %s s", dps, self.queue_timeout) + dps_responses[dps] = None + else: + dps_responses[dps] = response[0] return dps_responses async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]): From bf7c4dfbf25d6583d8eb7cfad86d573315fd625b Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 11 Apr 2024 09:17:42 -0400 Subject: [PATCH 12/13] fix: lint --- roborock/version_a01_apis/roborock_client_a01.py | 1 - 1 file changed, 1 deletion(-) diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 12d5b3e..5b42344 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -24,7 +24,6 @@ ZeoState, ) from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory -) from roborock.roborock_message import ( RoborockDyadDataProtocol, RoborockMessage, From 47b53266754cb6b8bec266656c48f7f81b7eecb2 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 11 Apr 2024 10:09:44 -0400 Subject: [PATCH 13/13] feat: add more zeo things --- roborock/code_mappings.py | 78 +++++++++++++++++++ roborock/roborock_message.py | 61 +++++++-------- .../version_a01_apis/roborock_client_a01.py | 26 ++++++- 3 files changed, 131 insertions(+), 34 deletions(-) diff --git a/roborock/code_mappings.py b/roborock/code_mappings.py index dd9a8aa..e5869fc 100644 --- a/roborock/code_mappings.py +++ b/roborock/code_mappings.py @@ -507,3 +507,81 @@ class ZeoProgram(RoborockEnum): twenty_c = 34 t_shirts = 35 stain_removal = 36 + + +class ZeoSoak(RoborockEnum): + normal = 0 + low = 1 + medium = 2 + high = 3 + max = 4 + + +class ZeoTemperature(RoborockEnum): + normal = 1 + low = 2 + medium = 3 + high = 4 + max = 5 + twenty_c = 6 + + +class ZeoRinse(RoborockEnum): + none = 0 + min = 1 + low = 2 + mid = 3 + high = 4 + max = 5 + + +class ZeoSpin(RoborockEnum): + none = 1 + very_low = 2 + low = 3 + mid = 4 + high = 5 + very_high = 6 + max = 7 + + +class ZeoDryingMode(RoborockEnum): + none = 0 + quick = 1 + iron = 2 + store = 3 + + +class ZeoDetergentType(RoborockEnum): + empty = 0 + low = 1 + medium = 2 + high = 3 + + +class ZeoSoftenerType(RoborockEnum): + empty = 0 + low = 1 + medium = 2 + high = 3 + + +class ZeoError(RoborockEnum): + none = 0 + refill_error = 1 + drain_error = 2 + door_lock_error = 3 + water_level_error = 4 + inverter_error = 5 + heating_error = 6 + temperature_error = 7 + communication_error = 10 + drying_error = 11 + drying_error_e_12 = 12 + drying_error_e_13 = 13 + drying_error_e_14 = 14 + drying_error_e_15 = 15 + drying_error_e_16 = 16 + drying_error_water_flow = 17 # Check for normal water flow + drying_error_restart = 18 # Restart the washer and try again + spin_error = 19 # re-arrange clothes diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index 230a1d5..7536770 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -88,37 +88,36 @@ class RoborockDyadDataProtocol(RoborockEnum): class RoborockZeoProtocol(RoborockEnum): - DRYING_STATUS = 134 - START = 200 - PAUSE = 201 - SHUTDOWN = 202 - STATE = 203 - MODE = 204 - PROGRAM = 205 - CHILD_LOCK = 206 - TEMP = 207 - RINSE_TIMES = 208 - SPIN_LEVEL = 209 - DRYING_MODE = 210 - DETERGENT_SET = 211 - SOFTENER_SET = 212 - DETERGENT_TYPE = 213 - SOFTENER_TYPE = 214 - COUNTDOWN = 217 - WASHING_LEFT = 218 - DOORLOCK_STATE = 219 - ERROR = 220 - CUSTOM_PARAM_SAVE = 221 - CUSTOM_PARAM_GET = 222 - SOUND_SET = 223 - TIMES_AFTER_CLEAN = 224 - DEFAULT_SETTING = 225 - DETERGENT_EMPTY = 226 - SOFTENER_EMPTY = 227 - LIGHT_SETTING = 229 - DETERGENT_VOLUME = 230 - SOFTENER_VOLUME = 231 - APP_AUTHORIZATION = 232 + START = 200 # rw + PAUSE = 201 # rw + SHUTDOWN = 202 # rw + STATE = 203 # ro + MODE = 204 # rw + PROGRAM = 205 # rw + CHILD_LOCK = 206 # rw + TEMP = 207 # rw + RINSE_TIMES = 208 # rw + SPIN_LEVEL = 209 # rw + DRYING_MODE = 210 # rw + DETERGENT_SET = 211 # rw + SOFTENER_SET = 212 # rw + DETERGENT_TYPE = 213 # rw + SOFTENER_TYPE = 214 # rw + COUNTDOWN = 217 # rw + WASHING_LEFT = 218 # ro + DOORLOCK_STATE = 219 # ro + ERROR = 220 # ro + CUSTOM_PARAM_SAVE = 221 # rw + CUSTOM_PARAM_GET = 222 # ro + SOUND_SET = 223 # rw + TIMES_AFTER_CLEAN = 224 # ro + DEFAULT_SETTING = 225 # rw + DETERGENT_EMPTY = 226 # ro + SOFTENER_EMPTY = 227 # ro + LIGHT_SETTING = 229 # rw + DETERGENT_VOLUME = 230 # rw + SOFTENER_VOLUME = 231 # rw + APP_AUTHORIZATION = 232 # rw ID_QUERY = 10000 F_C = 10001 SND_STATE = 10004 diff --git a/roborock/version_a01_apis/roborock_client_a01.py b/roborock/version_a01_apis/roborock_client_a01.py index 5b42344..c4d3cd3 100644 --- a/roborock/version_a01_apis/roborock_client_a01.py +++ b/roborock/version_a01_apis/roborock_client_a01.py @@ -19,9 +19,16 @@ DyadWarmLevel, DyadWaterLevel, RoborockDyadStateCode, + ZeoDetergentType, + ZeoDryingMode, + ZeoError, ZeoMode, ZeoProgram, + ZeoRinse, + ZeoSoftenerType, + ZeoSpin, ZeoState, + ZeoTemperature, ) from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory from roborock.roborock_message import ( @@ -73,11 +80,24 @@ class A01ProtocolCacheEntry: } zeo_data_protocol_entries = { - RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name), + # ro RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name), RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)), - RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val)), - RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: ZeoProgram(val)), + RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name), + RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)), + RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), + RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), + # rw + RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name), + RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name), + RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name), + RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name), + RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name), + RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name), + RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name), + RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name), + RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)), }