Skip to content

Commit e689e8d

Browse files
fix: using single device api
1 parent 2e8e307 commit e689e8d

File tree

5 files changed

+137
-168
lines changed

5 files changed

+137
-168
lines changed

roborock/api.py

Lines changed: 37 additions & 38 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, Mapping, Optional
17+
from typing import Any, Callable, Coroutine, 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, devices_info: Mapping[str, RoborockDeviceInfo]) -> None:
89-
self.devices_info = devices_info
88+
def __init__(self, endpoint: str, device_info: RoborockDeviceInfo) -> None:
89+
self.device_info = device_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, device_id: str, method: RoborockCommand, params: Optional[list] = None):
203+
async def send_command(self, method: RoborockCommand, params: Optional[list] = None):
204204
raise NotImplementedError
205205

206-
async def get_status(self, device_id: str) -> Status | None:
207-
status = await self.send_command(device_id, RoborockCommand.GET_STATUS)
206+
async def get_status(self) -> Status | None:
207+
status = await self.send_command(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, device_id: str) -> DNDTimer | None:
212+
async def get_dnd_timer(self) -> DNDTimer | None:
213213
try:
214-
dnd_timer = await self.send_command(device_id, RoborockCommand.GET_DND_TIMER)
214+
dnd_timer = await self.send_command(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, device_id: str) -> CleanSummary | None:
221+
async def get_clean_summary(self) -> CleanSummary | None:
222222
try:
223-
clean_summary = await self.send_command(device_id, RoborockCommand.GET_CLEAN_SUMMARY)
223+
clean_summary = await self.send_command(RoborockCommand.GET_CLEAN_SUMMARY)
224224
if isinstance(clean_summary, dict):
225225
return CleanSummary.from_dict(clean_summary)
226226
elif isinstance(clean_summary, list):
@@ -232,55 +232,54 @@ async def get_clean_summary(self, device_id: str) -> CleanSummary | None:
232232
_LOGGER.error(e)
233233
return None
234234

235-
async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord | None:
235+
async def get_clean_record(self, record_id: int) -> CleanRecord | None:
236236
try:
237-
clean_record = await self.send_command(device_id, RoborockCommand.GET_CLEAN_RECORD, [record_id])
237+
clean_record = await self.send_command(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, device_id: str) -> Consumable | None:
244+
async def get_consumable(self) -> Consumable | None:
245245
try:
246-
consumable = await self.send_command(device_id, RoborockCommand.GET_CONSUMABLE)
246+
consumable = await self.send_command(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, device_id: str) -> WashTowelMode | None:
253+
async def get_wash_towel_mode(self) -> WashTowelMode | None:
254254
try:
255-
washing_mode = await self.send_command(device_id, RoborockCommand.GET_WASH_TOWEL_MODE)
255+
washing_mode = await self.send_command(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, device_id: str) -> DustCollectionMode | None:
262+
async def get_dust_collection_mode(self) -> DustCollectionMode | None:
263263
try:
264-
dust_collection = await self.send_command(device_id, RoborockCommand.GET_DUST_COLLECTION_MODE)
264+
dust_collection = await self.send_command(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, device_id: str) -> SmartWashParams | None:
271+
async def get_smart_wash_params(self) -> SmartWashParams | None:
272272
try:
273-
mop_wash_mode = await self.send_command(device_id, RoborockCommand.GET_SMART_WASH_PARAMS)
273+
mop_wash_mode = await self.send_command(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, device_id: str, dock_type: RoborockEnum) -> DockSummary | None:
280+
async def get_dock_summary(self, 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
284283
:param dock_type: RoborockDockTypeCode"""
285284
try:
286285
commands: list[
@@ -289,11 +288,11 @@ async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> Doc
289288
Any,
290289
DustCollectionMode | WashTowelMode | SmartWashParams | None,
291290
]
292-
] = [self.get_dust_collection_mode(device_id)]
291+
] = [self.get_dust_collection_mode()]
293292
if dock_type == RoborockDockTypeCode["3"]:
294293
commands += [
295-
self.get_wash_towel_mode(device_id),
296-
self.get_smart_wash_params(device_id),
294+
self.get_wash_towel_mode(),
295+
self.get_smart_wash_params(),
297296
]
298297
[dust_collection_mode, wash_towel_mode, smart_wash_params] = unpack_list(
299298
list(await asyncio.gather(*commands)), 3
@@ -304,21 +303,21 @@ async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> Doc
304303
_LOGGER.error(e)
305304
return None
306305

307-
async def get_prop(self, device_id: str) -> DeviceProp | None:
306+
async def get_prop(self) -> DeviceProp | None:
308307
[status, dnd_timer, clean_summary, consumable] = await asyncio.gather(
309308
*[
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),
309+
self.get_status(),
310+
self.get_dnd_timer(),
311+
self.get_clean_summary(),
312+
self.get_consumable(),
314313
]
315314
)
316315
last_clean_record = None
317316
if clean_summary and clean_summary.records and len(clean_summary.records) > 0:
318-
last_clean_record = await self.get_clean_record(device_id, clean_summary.records[0])
317+
last_clean_record = await self.get_clean_record(clean_summary.records[0])
319318
dock_summary = None
320319
if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode["0"]:
321-
dock_summary = await self.get_dock_summary(device_id, status.dock_type)
320+
dock_summary = await self.get_dock_summary(status.dock_type)
322321
if any([status, dnd_timer, clean_summary, consumable]):
323322
return DeviceProp(
324323
status,
@@ -330,27 +329,27 @@ async def get_prop(self, device_id: str) -> DeviceProp | None:
330329
)
331330
return None
332331

333-
async def get_multi_maps_list(self, device_id) -> MultiMapsList | None:
332+
async def get_multi_maps_list(self) -> MultiMapsList | None:
334333
try:
335-
multi_maps_list = await self.send_command(device_id, RoborockCommand.GET_MULTI_MAPS_LIST)
334+
multi_maps_list = await self.send_command(RoborockCommand.GET_MULTI_MAPS_LIST)
336335
if isinstance(multi_maps_list, dict):
337336
return MultiMapsList.from_dict(multi_maps_list)
338337
except RoborockTimeout as e:
339338
_LOGGER.error(e)
340339
return None
341340

342-
async def get_networking(self, device_id) -> NetworkInfo | None:
341+
async def get_networking(self) -> NetworkInfo | None:
343342
try:
344-
networking_info = await self.send_command(device_id, RoborockCommand.GET_NETWORK_INFO)
343+
networking_info = await self.send_command(RoborockCommand.GET_NETWORK_INFO)
345344
if isinstance(networking_info, dict):
346345
return NetworkInfo.from_dict(networking_info)
347346
except RoborockTimeout as e:
348347
_LOGGER.error(e)
349348
return None
350349

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

roborock/cli.py

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

107110

108111
@click.command()
112+
@click.option("--device_id", required=True)
109113
@click.option("--cmd", required=True)
110114
@click.option("--params", required=False)
111115
@click.pass_context
112116
@run_sync()
113-
async def command(ctx, cmd, params):
117+
async def command(ctx, cmd, device_id, params):
114118
context: RoborockContext = ctx.obj
115119
login_data = context.login_data()
116120
if not login_data.home_data:
117121
await _discover(ctx)
118122
login_data = context.login_data()
119123
home_data = login_data.home_data
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)
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)
125129
mqtt_client.__del__()
126130

127131

roborock/cloud_api.py

Lines changed: 12 additions & 13 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 Mapping, Optional
8+
from typing import 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, devices_info: Mapping[str, RoborockDeviceInfo]) -> None:
28+
def __init__(self, user_data: UserData, device_info: 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, devices_info)
33+
RoborockClient.__init__(self, endpoint, device_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}/#"
66+
topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
6767
(result, mid) = self.subscribe(topic)
6868
if result != 0:
6969
message = f"Failed to subscribe (rc: {result})"
@@ -77,8 +77,7 @@ def on_connect(self, *args, **kwargs) -> None:
7777

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

8483
def on_disconnect(self, *args, **kwargs) -> None:
@@ -151,21 +150,21 @@ async def async_connect(self) -> None:
151150
async def validate_connection(self) -> None:
152151
await self.async_connect()
153152

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)
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)
156155
if info.rc != mqtt.MQTT_ERR_SUCCESS:
157156
raise RoborockException(f"Failed to publish (rc: {info.rc})")
158157

159-
async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None):
158+
async def send_command(self, method: RoborockCommand, params: Optional[list] = None):
160159
await self.validate_connection()
161160
request_id, timestamp, payload = super()._get_payload(method, params, True)
162161
_LOGGER.debug(f"id={request_id} Requesting method {method} with {params}")
163162
request_protocol = 101
164163
response_protocol = 301 if method in SPECIAL_COMMANDS else 102
165164
roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload)
166-
local_key = self.devices_info[device_id].device.local_key
165+
local_key = self.device_info.device.local_key
167166
msg = RoborockParser.encode(roborock_message, local_key)
168-
self._send_msg_raw(device_id, msg)
167+
self._send_msg_raw(msg)
169168
(response, err) = await self._async_response(request_id, response_protocol)
170169
if err:
171170
raise CommandVacuumError(method, err) from err
@@ -175,9 +174,9 @@ async def send_command(self, device_id: str, method: RoborockCommand, params: Op
175174
_LOGGER.debug(f"id={request_id} Response from {method}: {response}")
176175
return response
177176

178-
async def get_map_v1(self, device_id):
177+
async def get_map_v1(self):
179178
try:
180-
return await self.send_command(device_id, RoborockCommand.GET_MAP_V1)
179+
return await self.send_command(RoborockCommand.GET_MAP_V1)
181180
except RoborockException as e:
182181
_LOGGER.error(e)
183182
return None

0 commit comments

Comments
 (0)