|
1 |
| -import dataclasses |
2 |
| -import json |
3 | 1 | import logging
|
4 |
| -import typing |
5 | 2 | from abc import ABC, abstractmethod
|
6 | 3 | from collections.abc import Callable
|
7 | 4 | from datetime import time
|
8 |
| - |
9 |
| -from Crypto.Cipher import AES |
10 |
| -from Crypto.Util.Padding import unpad |
| 5 | +from typing import Any |
11 | 6 |
|
12 | 7 | from roborock import DeviceData
|
13 | 8 | from roborock.api import RoborockClient
|
|
33 | 28 | ZeoTemperature,
|
34 | 29 | )
|
35 | 30 | from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
|
| 31 | +from roborock.exceptions import RoborockException |
| 32 | +from roborock.protocols.a01_protocol import decode_rpc_response |
36 | 33 | from roborock.roborock_message import (
|
37 | 34 | RoborockDyadDataProtocol,
|
38 | 35 | RoborockMessage,
|
|
43 | 40 | _LOGGER = logging.getLogger(__name__)
|
44 | 41 |
|
45 | 42 |
|
46 |
| -@dataclasses.dataclass |
47 |
| -class A01ProtocolCacheEntry: |
48 |
| - post_process_fn: Callable |
49 |
| - value: typing.Any | None = None |
50 |
| - |
51 |
| - |
52 | 43 | # Right now this cache is not active, it was too much complexity for the initial addition of dyad.
|
53 |
| -protocol_entries = { |
54 |
| - RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name), |
55 |
| - RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name), |
56 |
| - RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name), |
57 |
| - RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name), |
58 |
| - RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name), |
59 |
| - RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name), |
60 |
| - RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name), |
61 |
| - RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name), |
62 |
| - RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)), |
63 |
| - RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)), |
64 |
| - RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), |
65 |
| - RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), |
66 |
| - RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name), |
67 |
| - RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)), |
68 |
| - RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)), |
69 |
| - RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), |
70 |
| - RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes |
71 |
| - RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), |
72 |
| - RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry( |
73 |
| - lambda val: time(hour=int(val / 60), minute=val % 60) |
| 44 | +DYAD_PROTOCOL_ENTRIES: dict[RoborockDyadDataProtocol, Callable] = { |
| 45 | + RoborockDyadDataProtocol.STATUS: lambda val: RoborockDyadStateCode(val).name, |
| 46 | + RoborockDyadDataProtocol.SELF_CLEAN_MODE: lambda val: DyadSelfCleanMode(val).name, |
| 47 | + RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: lambda val: DyadSelfCleanLevel(val).name, |
| 48 | + RoborockDyadDataProtocol.WARM_LEVEL: lambda val: DyadWarmLevel(val).name, |
| 49 | + RoborockDyadDataProtocol.CLEAN_MODE: lambda val: DyadCleanMode(val).name, |
| 50 | + RoborockDyadDataProtocol.SUCTION: lambda val: DyadSuction(val).name, |
| 51 | + RoborockDyadDataProtocol.WATER_LEVEL: lambda val: DyadWaterLevel(val).name, |
| 52 | + RoborockDyadDataProtocol.BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name, |
| 53 | + RoborockDyadDataProtocol.POWER: lambda val: int(val), |
| 54 | + RoborockDyadDataProtocol.AUTO_DRY: lambda val: bool(val), |
| 55 | + RoborockDyadDataProtocol.MESH_LEFT: lambda val: int(360000 - val * 60), |
| 56 | + RoborockDyadDataProtocol.BRUSH_LEFT: lambda val: int(360000 - val * 60), |
| 57 | + RoborockDyadDataProtocol.ERROR: lambda val: DyadError(val).name, |
| 58 | + RoborockDyadDataProtocol.VOLUME_SET: lambda val: int(val), |
| 59 | + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: lambda val: bool(val), |
| 60 | + RoborockDyadDataProtocol.AUTO_DRY_MODE: lambda val: bool(val), |
| 61 | + RoborockDyadDataProtocol.SILENT_DRY_DURATION: lambda val: int(val), # in minutes |
| 62 | + RoborockDyadDataProtocol.SILENT_MODE: lambda val: bool(val), |
| 63 | + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: lambda val: time( |
| 64 | + hour=int(val / 60), minute=val % 60 |
74 | 65 | ), # in minutes since 00:00
|
75 |
| - RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry( |
76 |
| - lambda val: time(hour=int(val / 60), minute=val % 60) |
| 66 | + RoborockDyadDataProtocol.SILENT_MODE_END_TIME: lambda val: time( |
| 67 | + hour=int(val / 60), minute=val % 60 |
77 | 68 | ), # in minutes since 00:00
|
78 |
| - RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry( |
79 |
| - lambda val: [int(v) for v in val.split(",")] |
80 |
| - ), # minutes of cleaning in past few days. |
81 |
| - RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)), |
82 |
| - RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)), |
83 |
| - RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)), |
| 69 | + RoborockDyadDataProtocol.RECENT_RUN_TIME: lambda val: [ |
| 70 | + int(v) for v in val.split(",") |
| 71 | + ], # minutes of cleaning in past few days. |
| 72 | + RoborockDyadDataProtocol.TOTAL_RUN_TIME: lambda val: int(val), |
| 73 | + RoborockDyadDataProtocol.SND_STATE: lambda val: DyadSndState.from_dict(val), |
| 74 | + RoborockDyadDataProtocol.PRODUCT_INFO: lambda val: DyadProductInfo.from_dict(val), |
84 | 75 | }
|
85 | 76 |
|
86 |
| -zeo_data_protocol_entries = { |
| 77 | +ZEO_PROTOCOL_ENTRIES: dict[RoborockZeoProtocol, Callable] = { |
87 | 78 | # ro
|
88 |
| - RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name), |
89 |
| - RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)), |
90 |
| - RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)), |
91 |
| - RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name), |
92 |
| - RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)), |
93 |
| - RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), |
94 |
| - RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), |
| 79 | + RoborockZeoProtocol.STATE: lambda val: ZeoState(val).name, |
| 80 | + RoborockZeoProtocol.COUNTDOWN: lambda val: int(val), |
| 81 | + RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val), |
| 82 | + RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, |
| 83 | + RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), |
| 84 | + RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), |
| 85 | + RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), |
95 | 86 | # rw
|
96 |
| - RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name), |
97 |
| - RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name), |
98 |
| - RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name), |
99 |
| - RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name), |
100 |
| - RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name), |
101 |
| - RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name), |
102 |
| - RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name), |
103 |
| - RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name), |
104 |
| - RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)), |
| 87 | + RoborockZeoProtocol.MODE: lambda val: ZeoMode(val).name, |
| 88 | + RoborockZeoProtocol.PROGRAM: lambda val: ZeoProgram(val).name, |
| 89 | + RoborockZeoProtocol.TEMP: lambda val: ZeoTemperature(val).name, |
| 90 | + RoborockZeoProtocol.RINSE_TIMES: lambda val: ZeoRinse(val).name, |
| 91 | + RoborockZeoProtocol.SPIN_LEVEL: lambda val: ZeoSpin(val).name, |
| 92 | + RoborockZeoProtocol.DRYING_MODE: lambda val: ZeoDryingMode(val).name, |
| 93 | + RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name, |
| 94 | + RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name, |
| 95 | + RoborockZeoProtocol.SOUND_SET: lambda val: bool(val), |
105 | 96 | }
|
106 | 97 |
|
107 | 98 |
|
| 99 | +def convert_dyad_value(protocol: int, value: Any) -> Any: |
| 100 | + """Convert a dyad protocol value to its corresponding type.""" |
| 101 | + protocol_value = RoborockDyadDataProtocol(protocol) |
| 102 | + if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None: |
| 103 | + return converter(value) |
| 104 | + return None |
| 105 | + |
| 106 | + |
| 107 | +def convert_zeo_value(protocol: int, value: Any) -> Any: |
| 108 | + """Convert a zeo protocol value to its corresponding type.""" |
| 109 | + protocol_value = RoborockZeoProtocol(protocol) |
| 110 | + if (converter := ZEO_PROTOCOL_ENTRIES.get(protocol_value)) is not None: |
| 111 | + return converter(value) |
| 112 | + return None |
| 113 | + |
| 114 | + |
108 | 115 | class RoborockClientA01(RoborockClient, ABC):
|
109 | 116 | """Roborock client base class for A01 devices."""
|
110 | 117 |
|
| 118 | + value_converter: Callable[[int, Any], Any] | None = None |
| 119 | + |
111 | 120 | def __init__(self, device_info: DeviceData, category: RoborockCategory):
|
112 | 121 | """Initialize the Roborock client."""
|
113 | 122 | super().__init__(device_info)
|
114 |
| - self.category = category |
| 123 | + if category == RoborockCategory.WET_DRY_VAC: |
| 124 | + self.value_converter = convert_dyad_value |
| 125 | + elif category == RoborockCategory.WASHING_MACHINE: |
| 126 | + self.value_converter = convert_zeo_value |
| 127 | + else: |
| 128 | + _LOGGER.debug("Device category %s is not (yet) supported", category) |
| 129 | + self.value_converter = None |
115 | 130 |
|
116 | 131 | def on_message_received(self, messages: list[RoborockMessage]) -> None:
|
| 132 | + if self.value_converter is None: |
| 133 | + return |
117 | 134 | for message in messages:
|
118 | 135 | protocol = message.protocol
|
119 | 136 | if message.payload and protocol in [
|
120 | 137 | RoborockMessageProtocol.RPC_RESPONSE,
|
121 | 138 | RoborockMessageProtocol.GENERAL_REQUEST,
|
122 | 139 | ]:
|
123 |
| - payload = message.payload |
124 | 140 | try:
|
125 |
| - payload = unpad(payload, AES.block_size) |
126 |
| - except Exception as err: |
127 |
| - self._logger.debug("Failed to unpad payload: %s", err) |
| 141 | + data_points = decode_rpc_response(message) |
| 142 | + except RoborockException as err: |
| 143 | + self._logger.error("Failed to decode message %s: %s", message, err) |
128 | 144 | continue
|
129 |
| - payload_json = json.loads(payload.decode()) |
130 |
| - for data_point_number, data_point in payload_json.get("dps").items(): |
131 |
| - data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol |
132 |
| - self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol) |
133 |
| - entries: dict |
134 |
| - if self.category == RoborockCategory.WET_DRY_VAC: |
135 |
| - data_point_protocol = RoborockDyadDataProtocol(int(data_point_number)) |
136 |
| - entries = protocol_entries |
137 |
| - elif self.category == RoborockCategory.WASHING_MACHINE: |
138 |
| - data_point_protocol = RoborockZeoProtocol(int(data_point_number)) |
139 |
| - entries = zeo_data_protocol_entries |
140 |
| - else: |
141 |
| - continue |
142 |
| - if data_point_protocol in entries: |
143 |
| - # Auto convert into data struct we want. |
144 |
| - converted_response = entries[data_point_protocol].post_process_fn(data_point) |
| 145 | + for data_point_number, data_point in data_points.items(): |
| 146 | + if converted_response := self.value_converter(data_point_number, data_point): |
145 | 147 | queue = self._waiting_queue.get(int(data_point_number))
|
146 | 148 | if queue and queue.protocol == protocol:
|
147 | 149 | queue.set_result(converted_response)
|
| 150 | + else: |
| 151 | + self._logger.warning( |
| 152 | + "Received unknown data point %s for protocol %s, ignoring", data_point_number, protocol |
| 153 | + ) |
148 | 154 |
|
149 | 155 | @abstractmethod
|
150 | 156 | async def update_values(
|
151 | 157 | self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
|
152 |
| - ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]: |
| 158 | + ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any]: |
153 | 159 | """This should handle updating for each given protocol."""
|
0 commit comments