Skip to content

Commit 49d77f8

Browse files
authored
fix: add functionality for missing enum values (#43)
* fix: add functionality for missing enum values * fix: temp removed 207 * Revert "chore: linting" This reverts commit 58b4683. * Revert "chore: linting" This reverts commit 2ed367c. * Revert "fix: using single device api" This reverts commit e689e8d.
1 parent 58b4683 commit 49d77f8

File tree

8 files changed

+189
-141
lines changed

8 files changed

+189
-141
lines changed

roborock/api.py

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import struct
1515
import time
1616
from random import randint
17-
from typing import Any, Callable, Coroutine, Optional
17+
from typing import Any, Callable, Coroutine, Mapping, Optional
1818

1919
import aiohttp
2020
from Crypto.Cipher import AES
@@ -85,8 +85,8 @@ async def request(self, method: str, url: str, params=None, data=None, headers=N
8585

8686

8787
class RoborockClient:
88-
def __init__(self, endpoint: str, device_info: RoborockDeviceInfo) -> None:
89-
self.device_info = device_info
88+
def __init__(self, endpoint: str, devices_info: Mapping[str, RoborockDeviceInfo]) -> None:
89+
self.devices_info = devices_info
9090
self._endpoint = endpoint
9191
self._nonce = secrets.token_bytes(16)
9292
self._waiting_queue: dict[int, RoborockFuture] = {}
@@ -200,27 +200,27 @@ def _get_payload(self, method: RoborockCommand, params: Optional[list] = None, s
200200
)
201201
return request_id, timestamp, payload
202202

203-
async def send_command(self, method: RoborockCommand, params: Optional[list] = None):
203+
async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None):
204204
raise NotImplementedError
205205

206-
async def get_status(self) -> Status | None:
207-
status = await self.send_command(RoborockCommand.GET_STATUS)
206+
async def get_status(self, device_id: str) -> Status | None:
207+
status = await self.send_command(device_id, RoborockCommand.GET_STATUS)
208208
if isinstance(status, dict):
209209
return Status.from_dict(status)
210210
return None
211211

212-
async def get_dnd_timer(self) -> DNDTimer | None:
212+
async def get_dnd_timer(self, device_id: str) -> DNDTimer | None:
213213
try:
214-
dnd_timer = await self.send_command(RoborockCommand.GET_DND_TIMER)
214+
dnd_timer = await self.send_command(device_id, RoborockCommand.GET_DND_TIMER)
215215
if isinstance(dnd_timer, dict):
216216
return DNDTimer.from_dict(dnd_timer)
217217
except RoborockTimeout as e:
218218
_LOGGER.error(e)
219219
return None
220220

221-
async def get_clean_summary(self) -> CleanSummary | None:
221+
async def get_clean_summary(self, device_id: str) -> CleanSummary | None:
222222
try:
223-
clean_summary = await self.send_command(RoborockCommand.GET_CLEAN_SUMMARY)
223+
clean_summary = await self.send_command(device_id, RoborockCommand.GET_CLEAN_SUMMARY)
224224
if isinstance(clean_summary, dict):
225225
return CleanSummary.from_dict(clean_summary)
226226
elif isinstance(clean_summary, list):
@@ -232,54 +232,55 @@ async def get_clean_summary(self) -> CleanSummary | None:
232232
_LOGGER.error(e)
233233
return None
234234

235-
async def get_clean_record(self, record_id: int) -> CleanRecord | None:
235+
async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord | None:
236236
try:
237-
clean_record = await self.send_command(RoborockCommand.GET_CLEAN_RECORD, [record_id])
237+
clean_record = await self.send_command(device_id, RoborockCommand.GET_CLEAN_RECORD, [record_id])
238238
if isinstance(clean_record, dict):
239239
return CleanRecord.from_dict(clean_record)
240240
except RoborockTimeout as e:
241241
_LOGGER.error(e)
242242
return None
243243

244-
async def get_consumable(self) -> Consumable | None:
244+
async def get_consumable(self, device_id: str) -> Consumable | None:
245245
try:
246-
consumable = await self.send_command(RoborockCommand.GET_CONSUMABLE)
246+
consumable = await self.send_command(device_id, RoborockCommand.GET_CONSUMABLE)
247247
if isinstance(consumable, dict):
248248
return Consumable.from_dict(consumable)
249249
except RoborockTimeout as e:
250250
_LOGGER.error(e)
251251
return None
252252

253-
async def get_wash_towel_mode(self) -> WashTowelMode | None:
253+
async def get_wash_towel_mode(self, device_id: str) -> WashTowelMode | None:
254254
try:
255-
washing_mode = await self.send_command(RoborockCommand.GET_WASH_TOWEL_MODE)
255+
washing_mode = await self.send_command(device_id, RoborockCommand.GET_WASH_TOWEL_MODE)
256256
if isinstance(washing_mode, dict):
257257
return WashTowelMode.from_dict(washing_mode)
258258
except RoborockTimeout as e:
259259
_LOGGER.error(e)
260260
return None
261261

262-
async def get_dust_collection_mode(self) -> DustCollectionMode | None:
262+
async def get_dust_collection_mode(self, device_id: str) -> DustCollectionMode | None:
263263
try:
264-
dust_collection = await self.send_command(RoborockCommand.GET_DUST_COLLECTION_MODE)
264+
dust_collection = await self.send_command(device_id, RoborockCommand.GET_DUST_COLLECTION_MODE)
265265
if isinstance(dust_collection, dict):
266266
return DustCollectionMode.from_dict(dust_collection)
267267
except RoborockTimeout as e:
268268
_LOGGER.error(e)
269269
return None
270270

271-
async def get_smart_wash_params(self) -> SmartWashParams | None:
271+
async def get_smart_wash_params(self, device_id: str) -> SmartWashParams | None:
272272
try:
273-
mop_wash_mode = await self.send_command(RoborockCommand.GET_SMART_WASH_PARAMS)
273+
mop_wash_mode = await self.send_command(device_id, RoborockCommand.GET_SMART_WASH_PARAMS)
274274
if isinstance(mop_wash_mode, dict):
275275
return SmartWashParams.from_dict(mop_wash_mode)
276276
except RoborockTimeout as e:
277277
_LOGGER.error(e)
278278
return None
279279

280-
async def get_dock_summary(self, dock_type: RoborockEnum) -> DockSummary | None:
280+
async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> DockSummary | None:
281281
"""Gets the status summary from the dock with the methods available for a given dock.
282282
283+
:param device_id: Device id
283284
:param dock_type: RoborockDockTypeCode"""
284285
try:
285286
commands: list[
@@ -288,11 +289,11 @@ async def get_dock_summary(self, dock_type: RoborockEnum) -> DockSummary | None:
288289
Any,
289290
DustCollectionMode | WashTowelMode | SmartWashParams | None,
290291
]
291-
] = [self.get_dust_collection_mode()]
292+
] = [self.get_dust_collection_mode(device_id)]
292293
if dock_type == RoborockDockTypeCode["3"]:
293294
commands += [
294-
self.get_wash_towel_mode(),
295-
self.get_smart_wash_params(),
295+
self.get_wash_towel_mode(device_id),
296+
self.get_smart_wash_params(device_id),
296297
]
297298
[dust_collection_mode, wash_towel_mode, smart_wash_params] = unpack_list(
298299
list(await asyncio.gather(*commands)), 3
@@ -303,21 +304,21 @@ async def get_dock_summary(self, dock_type: RoborockEnum) -> DockSummary | None:
303304
_LOGGER.error(e)
304305
return None
305306

306-
async def get_prop(self) -> DeviceProp | None:
307+
async def get_prop(self, device_id: str) -> DeviceProp | None:
307308
[status, dnd_timer, clean_summary, consumable] = await asyncio.gather(
308309
*[
309-
self.get_status(),
310-
self.get_dnd_timer(),
311-
self.get_clean_summary(),
312-
self.get_consumable(),
310+
self.get_status(device_id),
311+
self.get_dnd_timer(device_id),
312+
self.get_clean_summary(device_id),
313+
self.get_consumable(device_id),
313314
]
314315
)
315316
last_clean_record = None
316317
if clean_summary and clean_summary.records and len(clean_summary.records) > 0:
317-
last_clean_record = await self.get_clean_record(clean_summary.records[0])
318+
last_clean_record = await self.get_clean_record(device_id, clean_summary.records[0])
318319
dock_summary = None
319320
if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode["0"]:
320-
dock_summary = await self.get_dock_summary(status.dock_type)
321+
dock_summary = await self.get_dock_summary(device_id, status.dock_type)
321322
if any([status, dnd_timer, clean_summary, consumable]):
322323
return DeviceProp(
323324
status,
@@ -329,27 +330,27 @@ async def get_prop(self) -> DeviceProp | None:
329330
)
330331
return None
331332

332-
async def get_multi_maps_list(self) -> MultiMapsList | None:
333+
async def get_multi_maps_list(self, device_id) -> MultiMapsList | None:
333334
try:
334-
multi_maps_list = await self.send_command(RoborockCommand.GET_MULTI_MAPS_LIST)
335+
multi_maps_list = await self.send_command(device_id, RoborockCommand.GET_MULTI_MAPS_LIST)
335336
if isinstance(multi_maps_list, dict):
336337
return MultiMapsList.from_dict(multi_maps_list)
337338
except RoborockTimeout as e:
338339
_LOGGER.error(e)
339340
return None
340341

341-
async def get_networking(self) -> NetworkInfo | None:
342+
async def get_networking(self, device_id) -> NetworkInfo | None:
342343
try:
343-
networking_info = await self.send_command(RoborockCommand.GET_NETWORK_INFO)
344+
networking_info = await self.send_command(device_id, RoborockCommand.GET_NETWORK_INFO)
344345
if isinstance(networking_info, dict):
345346
return NetworkInfo.from_dict(networking_info)
346347
except RoborockTimeout as e:
347348
_LOGGER.error(e)
348349
return None
349350

350-
async def get_room_mapping(self) -> list[RoomMapping]:
351+
async def get_room_mapping(self, device_id: str) -> list[RoomMapping]:
351352
"""Gets the mapping from segment id -> iot id. Only works on local api."""
352-
mapping = await self.send_command(RoborockCommand.GET_ROOM_MAPPING)
353+
mapping = await self.send_command(device_id, RoborockCommand.GET_ROOM_MAPPING)
353354
if isinstance(mapping, list):
354355
return [
355356
RoomMapping(segment_id=segment_id, iot_id=iot_id) # type: ignore

roborock/cli.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,30 +102,26 @@ async def list_devices(ctx):
102102
await _discover(ctx)
103103
login_data = context.login_data()
104104
home_data = login_data.home_data
105-
device_name_id = ", ".join(
106-
[f"{device.name}: {device.duid}" for device in home_data.devices + home_data.received_devices]
107-
)
108-
click.echo(f"Known devices {device_name_id}")
105+
click.echo(f"Known devices {', '.join([device.name for device in home_data.devices + home_data.received_devices])}")
109106

110107

111108
@click.command()
112-
@click.option("--device_id", required=True)
113109
@click.option("--cmd", required=True)
114110
@click.option("--params", required=False)
115111
@click.pass_context
116112
@run_sync()
117-
async def command(ctx, cmd, device_id, params):
113+
async def command(ctx, cmd, params):
118114
context: RoborockContext = ctx.obj
119115
login_data = context.login_data()
120116
if not login_data.home_data:
121117
await _discover(ctx)
122118
login_data = context.login_data()
123119
home_data = login_data.home_data
124-
devices = home_data.devices + home_data.received_devices
125-
device = next((device for device in devices if device.duid == device_id), None)
126-
device_info = RoborockDeviceInfo(device=device)
127-
mqtt_client = RoborockMqttClient(login_data.user_data, device_info)
128-
await mqtt_client.send_command(cmd, params)
120+
device_map: dict[str, RoborockDeviceInfo] = {}
121+
for device in home_data.devices + home_data.received_devices:
122+
device_map[device.duid] = RoborockDeviceInfo(device=device)
123+
mqtt_client = RoborockMqttClient(login_data.user_data, device_map)
124+
await mqtt_client.send_command(home_data.devices[0].duid, cmd, params)
129125
mqtt_client.__del__()
130126

131127

roborock/cloud_api.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import threading
66
import uuid
77
from asyncio import Lock
8-
from typing import Optional
8+
from typing import Mapping, Optional
99
from urllib.parse import urlparse
1010

1111
import paho.mqtt.client as mqtt
@@ -25,12 +25,12 @@
2525
class RoborockMqttClient(RoborockClient, mqtt.Client):
2626
_thread: threading.Thread
2727

28-
def __init__(self, user_data: UserData, device_info: RoborockDeviceInfo) -> None:
28+
def __init__(self, user_data: UserData, devices_info: Mapping[str, RoborockDeviceInfo]) -> None:
2929
rriot = user_data.rriot
3030
if rriot is None:
3131
raise RoborockException("Got no rriot data from user_data")
3232
endpoint = base64.b64encode(md5bin(rriot.k)[8:14]).decode()
33-
RoborockClient.__init__(self, endpoint, device_info)
33+
RoborockClient.__init__(self, endpoint, devices_info)
3434
mqtt.Client.__init__(self, protocol=mqtt.MQTTv5)
3535
self._mqtt_user = rriot.u
3636
self._hashed_user = md5hex(self._mqtt_user + ":" + rriot.k)[2:10]
@@ -63,7 +63,7 @@ def on_connect(self, *args, **kwargs) -> None:
6363
connection_queue.resolve((None, VacuumError(rc, message)))
6464
return
6565
_LOGGER.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}")
66-
topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
66+
topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/#"
6767
(result, mid) = self.subscribe(topic)
6868
if result != 0:
6969
message = f"Failed to subscribe (rc: {result})"
@@ -77,7 +77,8 @@ def on_connect(self, *args, **kwargs) -> None:
7777

7878
def on_message(self, *args, **kwargs) -> None:
7979
_, __, msg = args
80-
messages, _ = RoborockParser.decode(msg.payload, self.device_info.device.local_key)
80+
device_id = msg.topic.split("/").pop()
81+
messages, _ = RoborockParser.decode(msg.payload, self.devices_info[device_id].device.local_key)
8182
super().on_message(messages)
8283

8384
def on_disconnect(self, *args, **kwargs) -> None:
@@ -150,21 +151,21 @@ async def async_connect(self) -> None:
150151
async def validate_connection(self) -> None:
151152
await self.async_connect()
152153

153-
def _send_msg_raw(self, msg) -> None:
154-
info = self.publish(f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}", msg)
154+
def _send_msg_raw(self, device_id, msg) -> None:
155+
info = self.publish(f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{device_id}", msg)
155156
if info.rc != mqtt.MQTT_ERR_SUCCESS:
156157
raise RoborockException(f"Failed to publish (rc: {info.rc})")
157158

158-
async def send_command(self, method: RoborockCommand, params: Optional[list] = None):
159+
async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None):
159160
await self.validate_connection()
160161
request_id, timestamp, payload = super()._get_payload(method, params, True)
161162
_LOGGER.debug(f"id={request_id} Requesting method {method} with {params}")
162163
request_protocol = 101
163164
response_protocol = 301 if method in SPECIAL_COMMANDS else 102
164165
roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload)
165-
local_key = self.device_info.device.local_key
166+
local_key = self.devices_info[device_id].device.local_key
166167
msg = RoborockParser.encode(roborock_message, local_key)
167-
self._send_msg_raw(msg)
168+
self._send_msg_raw(device_id, msg)
168169
(response, err) = await self._async_response(request_id, response_protocol)
169170
if err:
170171
raise CommandVacuumError(method, err) from err
@@ -174,9 +175,9 @@ async def send_command(self, method: RoborockCommand, params: Optional[list] = N
174175
_LOGGER.debug(f"id={request_id} Response from {method}: {response}")
175176
return response
176177

177-
async def get_map_v1(self):
178+
async def get_map_v1(self, device_id):
178179
try:
179-
return await self.send_command(RoborockCommand.GET_MAP_V1)
180+
return await self.send_command(device_id, RoborockCommand.GET_MAP_V1)
180181
except RoborockException as e:
181182
_LOGGER.error(e)
182183
return None

roborock/code_mappings.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

3+
import logging
34
from enum import Enum
45
from typing import Any, Type, TypeVar
56

67
_StrEnumT = TypeVar("_StrEnumT", bound="RoborockEnum")
78

9+
_LOGGER = logging.getLogger(__name__)
10+
811

912
class RoborockEnum(str, Enum):
1013
def __new__(cls: Type[_StrEnumT], value: str, *args: Any, **kwargs: Any) -> _StrEnumT:
@@ -18,11 +21,15 @@ def __str__(self):
1821

1922
@classmethod
2023
def _missing_(cls: Type[_StrEnumT], code: object):
21-
return cls._member_map_.get(str(code))
24+
if cls._member_map_.get(str(code)):
25+
return cls._member_map_.get(str(code))
26+
else:
27+
_LOGGER.warning(f"Unknown code {code} for {cls.__name__}")
28+
return cls._member_map_.get(str(-9999))
2229

2330
@classmethod
2431
def as_dict(cls: Type[_StrEnumT]):
25-
return {int(i.name): i.value for i in cls}
32+
return {int(i.name): i.value for i in cls if i.value != "UNKNOWN"}
2633

2734
@classmethod
2835
def values(cls: Type[_StrEnumT]):
@@ -42,6 +49,7 @@ def __getitem__(cls: Type[_StrEnumT], item):
4249

4350

4451
def create_code_enum(name: str, data: dict) -> RoborockEnum:
52+
data[-9999] = "UNKNOWN"
4553
return RoborockEnum(name, {str(key): value for key, value in data.items()})
4654

4755

0 commit comments

Comments
 (0)