python-roborock 2.21.0__tar.gz → 2.23.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-2.21.0 → python_roborock-2.23.0}/PKG-INFO +1 -1
- {python_roborock-2.21.0 → python_roborock-2.23.0}/pyproject.toml +2 -2
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/cli.py +54 -2
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/cloud_api.py +13 -13
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/mqtt/roborock_session.py +3 -4
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/protocol.py +22 -1
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +13 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/LICENSE +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/README.md +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/__init__.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/api.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/code_mappings.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/const.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/containers.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/devices/device.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/local_api.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/py.typed +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/util.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.23.0"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
|
|
6
6
|
license = "GPL-3.0-only"
|
|
@@ -75,4 +75,4 @@ select=["E", "F", "UP", "I"]
|
|
|
75
75
|
[tool.pytest.ini_options]
|
|
76
76
|
asyncio_mode = "auto"
|
|
77
77
|
asyncio_default_fixture_loop_scope = "function"
|
|
78
|
-
timeout =
|
|
78
|
+
timeout = 30
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
from pathlib import Path
|
|
@@ -12,7 +13,8 @@ from pyshark.packet.packet import Packet # type: ignore
|
|
|
12
13
|
|
|
13
14
|
from roborock import RoborockException
|
|
14
15
|
from roborock.containers import DeviceData, HomeDataProduct, LoginData
|
|
15
|
-
from roborock.
|
|
16
|
+
from roborock.mqtt.roborock_session import create_mqtt_session
|
|
17
|
+
from roborock.protocol import MessageParser, create_mqtt_params
|
|
16
18
|
from roborock.util import run_sync
|
|
17
19
|
from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
|
|
18
20
|
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
|
@@ -45,7 +47,8 @@ class RoborockContext:
|
|
|
45
47
|
if self._login_data is None:
|
|
46
48
|
raise RoborockException("You must login first")
|
|
47
49
|
|
|
48
|
-
def login_data(self):
|
|
50
|
+
def login_data(self) -> LoginData:
|
|
51
|
+
"""Get the login data."""
|
|
49
52
|
self.validate()
|
|
50
53
|
return self._login_data
|
|
51
54
|
|
|
@@ -90,6 +93,54 @@ async def login(ctx, email, password):
|
|
|
90
93
|
context.update(LoginData(user_data=user_data, email=email))
|
|
91
94
|
|
|
92
95
|
|
|
96
|
+
@click.command()
|
|
97
|
+
@click.pass_context
|
|
98
|
+
@click.option("--duration", default=10, help="Duration to run the MQTT session in seconds")
|
|
99
|
+
@run_sync()
|
|
100
|
+
async def session(ctx, duration: int):
|
|
101
|
+
context: RoborockContext = ctx.obj
|
|
102
|
+
login_data = context.login_data()
|
|
103
|
+
|
|
104
|
+
# Discovery devices if not already available
|
|
105
|
+
if not login_data.home_data:
|
|
106
|
+
await _discover(ctx)
|
|
107
|
+
login_data = context.login_data()
|
|
108
|
+
if not login_data.home_data or not login_data.home_data.devices:
|
|
109
|
+
raise RoborockException("Unable to discover devices")
|
|
110
|
+
|
|
111
|
+
all_devices = login_data.home_data.devices + login_data.home_data.received_devices
|
|
112
|
+
click.echo(f"Discovered devices: {', '.join([device.name for device in all_devices])}")
|
|
113
|
+
|
|
114
|
+
rriot = login_data.user_data.rriot
|
|
115
|
+
params = create_mqtt_params(rriot)
|
|
116
|
+
|
|
117
|
+
mqtt_session = await create_mqtt_session(params)
|
|
118
|
+
click.echo("Starting MQTT session...")
|
|
119
|
+
if not mqtt_session.connected:
|
|
120
|
+
raise RoborockException("Failed to connect to MQTT broker")
|
|
121
|
+
|
|
122
|
+
def on_message(bytes: bytes):
|
|
123
|
+
"""Callback function to handle incoming MQTT messages."""
|
|
124
|
+
# Decode the first 20 bytes of the message for display
|
|
125
|
+
bytes = bytes[:20]
|
|
126
|
+
|
|
127
|
+
click.echo(f"Received message: {bytes}...")
|
|
128
|
+
|
|
129
|
+
unsubs = []
|
|
130
|
+
for device in all_devices:
|
|
131
|
+
device_topic = f"rr/m/o/{rriot.u}/{params.username}/{device.duid}"
|
|
132
|
+
unsub = await mqtt_session.subscribe(device_topic, on_message)
|
|
133
|
+
unsubs.append(unsub)
|
|
134
|
+
|
|
135
|
+
click.echo("MQTT session started. Listening for messages...")
|
|
136
|
+
await asyncio.sleep(duration)
|
|
137
|
+
|
|
138
|
+
click.echo("Stopping MQTT session...")
|
|
139
|
+
for unsub in unsubs:
|
|
140
|
+
unsub()
|
|
141
|
+
await mqtt_session.close()
|
|
142
|
+
|
|
143
|
+
|
|
93
144
|
async def _discover(ctx):
|
|
94
145
|
context: RoborockContext = ctx.obj
|
|
95
146
|
login_data = context.login_data()
|
|
@@ -264,6 +315,7 @@ cli.add_command(execute_scene)
|
|
|
264
315
|
cli.add_command(status)
|
|
265
316
|
cli.add_command(command)
|
|
266
317
|
cli.add_command(parser)
|
|
318
|
+
cli.add_command(session)
|
|
267
319
|
|
|
268
320
|
|
|
269
321
|
def main():
|
|
@@ -6,14 +6,19 @@ import threading
|
|
|
6
6
|
from abc import ABC
|
|
7
7
|
from asyncio import Lock
|
|
8
8
|
from typing import Any
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
9
|
|
|
11
10
|
import paho.mqtt.client as mqtt
|
|
12
11
|
|
|
13
12
|
from .api import KEEPALIVE, RoborockClient
|
|
14
13
|
from .containers import DeviceData, UserData
|
|
15
14
|
from .exceptions import RoborockException, VacuumError
|
|
16
|
-
from .protocol import
|
|
15
|
+
from .protocol import (
|
|
16
|
+
Decoder,
|
|
17
|
+
Encoder,
|
|
18
|
+
create_mqtt_decoder,
|
|
19
|
+
create_mqtt_encoder,
|
|
20
|
+
create_mqtt_params,
|
|
21
|
+
)
|
|
17
22
|
from .roborock_future import RoborockFuture
|
|
18
23
|
|
|
19
24
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -53,25 +58,20 @@ class RoborockMqttClient(RoborockClient, ABC):
|
|
|
53
58
|
if rriot is None:
|
|
54
59
|
raise RoborockException("Got no rriot data from user_data")
|
|
55
60
|
RoborockClient.__init__(self, device_info)
|
|
61
|
+
mqtt_params = create_mqtt_params(rriot)
|
|
56
62
|
self._mqtt_user = rriot.u
|
|
57
|
-
self._hashed_user =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
raise RoborockException("Url parsing returned an invalid hostname")
|
|
61
|
-
self._mqtt_host = str(url.hostname)
|
|
62
|
-
self._mqtt_port = url.port
|
|
63
|
-
self._mqtt_ssl = url.scheme == "ssl"
|
|
63
|
+
self._hashed_user = mqtt_params.username
|
|
64
|
+
self._mqtt_host = mqtt_params.host
|
|
65
|
+
self._mqtt_port = mqtt_params.port
|
|
64
66
|
|
|
65
67
|
self._mqtt_client = _Mqtt()
|
|
66
68
|
self._mqtt_client.on_connect = self._mqtt_on_connect
|
|
67
69
|
self._mqtt_client.on_message = self._mqtt_on_message
|
|
68
70
|
self._mqtt_client.on_disconnect = self._mqtt_on_disconnect
|
|
69
|
-
if
|
|
71
|
+
if mqtt_params.tls:
|
|
70
72
|
self._mqtt_client.tls_set()
|
|
71
73
|
|
|
72
|
-
self.
|
|
73
|
-
self._hashed_password = md5hex(self._mqtt_password + ":" + rriot.k)[16:]
|
|
74
|
-
self._mqtt_client.username_pw_set(self._hashed_user, self._hashed_password)
|
|
74
|
+
self._mqtt_client.username_pw_set(mqtt_params.username, mqtt_params.password)
|
|
75
75
|
self._waiting_queue: dict[int, RoborockFuture] = {}
|
|
76
76
|
self._mutex = Lock()
|
|
77
77
|
self._decoder: Decoder = create_mqtt_decoder(device_info.device.local_key)
|
|
@@ -116,9 +116,9 @@ class RoborockMqttSession(MqttSession):
|
|
|
116
116
|
_LOGGER.info("MQTT error: %s", err)
|
|
117
117
|
except asyncio.CancelledError as err:
|
|
118
118
|
if start_future:
|
|
119
|
-
_LOGGER.debug("MQTT loop was cancelled")
|
|
119
|
+
_LOGGER.debug("MQTT loop was cancelled while starting")
|
|
120
120
|
start_future.set_exception(err)
|
|
121
|
-
_LOGGER.debug("MQTT loop was cancelled
|
|
121
|
+
_LOGGER.debug("MQTT loop was cancelled")
|
|
122
122
|
return
|
|
123
123
|
# Catch exceptions to avoid crashing the loop
|
|
124
124
|
# and to allow the loop to retry.
|
|
@@ -171,8 +171,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
171
171
|
self._client = None
|
|
172
172
|
|
|
173
173
|
async def _process_message_loop(self, client: aiomqtt.Client) -> None:
|
|
174
|
-
_LOGGER.debug("
|
|
175
|
-
_LOGGER.debug("Processing MQTT messages: %s", client.messages)
|
|
174
|
+
_LOGGER.debug("Processing MQTT messages")
|
|
176
175
|
async for message in client.messages:
|
|
177
176
|
_LOGGER.debug("Received message: %s", message)
|
|
178
177
|
for listener in self._listeners.get(message.topic.value, []):
|
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
from asyncio import BaseTransport, Lock
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
+
from urllib.parse import urlparse
|
|
11
12
|
|
|
12
13
|
from construct import ( # type: ignore
|
|
13
14
|
Bytes,
|
|
@@ -30,7 +31,9 @@ from construct import ( # type: ignore
|
|
|
30
31
|
from Crypto.Cipher import AES
|
|
31
32
|
from Crypto.Util.Padding import pad, unpad
|
|
32
33
|
|
|
33
|
-
from roborock import BroadcastMessage,
|
|
34
|
+
from roborock.containers import BroadcastMessage, RRiot
|
|
35
|
+
from roborock.exceptions import RoborockException
|
|
36
|
+
from roborock.mqtt.session import MqttParams
|
|
34
37
|
from roborock.roborock_message import RoborockMessage
|
|
35
38
|
|
|
36
39
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -361,6 +364,24 @@ MessageParser: _Parser = _Parser(_Messages, True)
|
|
|
361
364
|
BroadcastParser: _Parser = _Parser(_BroadcastMessage, False)
|
|
362
365
|
|
|
363
366
|
|
|
367
|
+
def create_mqtt_params(rriot: RRiot) -> MqttParams:
|
|
368
|
+
"""Return the MQTT parameters for this user."""
|
|
369
|
+
url = urlparse(rriot.r.m)
|
|
370
|
+
if not isinstance(url.hostname, str):
|
|
371
|
+
raise RoborockException(f"Url parsing '{rriot.r.m}' returned an invalid hostname")
|
|
372
|
+
if not url.port:
|
|
373
|
+
raise RoborockException(f"Url parsing '{rriot.r.m}' returned an invalid port")
|
|
374
|
+
hashed_user = md5hex(rriot.u + ":" + rriot.k)[2:10]
|
|
375
|
+
hashed_password = md5hex(rriot.s + ":" + rriot.k)[16:]
|
|
376
|
+
return MqttParams(
|
|
377
|
+
host=str(url.hostname),
|
|
378
|
+
port=url.port,
|
|
379
|
+
tls=(url.scheme == "ssl"),
|
|
380
|
+
username=hashed_user,
|
|
381
|
+
password=hashed_password,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
364
385
|
Decoder = Callable[[bytes], list[RoborockMessage]]
|
|
365
386
|
Encoder = Callable[[RoborockMessage], bytes]
|
|
366
387
|
|
|
@@ -73,3 +73,16 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
|
|
|
73
73
|
payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
|
|
74
74
|
)
|
|
75
75
|
)
|
|
76
|
+
|
|
77
|
+
async def set_value(
|
|
78
|
+
self, protocol: RoborockDyadDataProtocol | RoborockZeoProtocol, value: typing.Any
|
|
79
|
+
) -> dict[int, typing.Any]:
|
|
80
|
+
"""Set a value for a specific protocol on the A01 device."""
|
|
81
|
+
payload = {"dps": {int(protocol): value}}
|
|
82
|
+
return await self.send_message(
|
|
83
|
+
RoborockMessage(
|
|
84
|
+
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
85
|
+
version=b"A01",
|
|
86
|
+
payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
|
|
87
|
+
)
|
|
88
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.21.0 → python_roborock-2.23.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|