Skip to content

Commit 01ec906

Browse files
authored
Merge branch 'main' into roborock_supported_features_auto
2 parents c8043cb + cbd6df2 commit 01ec906

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4167
-376
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
run: poetry install
5151
shell: bash
5252
- name: Test with Pytest
53-
run: poetry run pytest
53+
run: poetry run pytest --log-cli-level=DEBUG -vv -s
5454
shell: bash
5555
release:
5656
runs-on: ubuntu-latest
@@ -75,7 +75,7 @@ jobs:
7575
persist-credentials: false
7676
- name: Python Semantic Release
7777
id: release
78-
uses: python-semantic-release/python-semantic-release@v9.21.0
78+
uses: python-semantic-release/python-semantic-release@v10.2.0
7979
with:
8080
github_token: ${{ secrets.GH_TOKEN }}
8181

CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,57 @@
11
# CHANGELOG
22

33

4+
## v2.19.0 (2025-05-13)
5+
6+
### Bug Fixes
7+
8+
- Add Saros 10 dock type code ([#362](https://github.com/Python-roborock/python-roborock/pull/362),
9+
[`240bf59`](https://github.com/Python-roborock/python-roborock/commit/240bf59df1873e85e05356496e5be01f1a000199))
10+
11+
### Chores
12+
13+
- **deps**: Bump aiomqtt from 2.3.2 to 2.4.0
14+
([#375](https://github.com/Python-roborock/python-roborock/pull/375),
15+
[`b243a25`](https://github.com/Python-roborock/python-roborock/commit/b243a25569c2cb6b54e6c0e1eed6dadecb9ad84c))
16+
17+
Bumps [aiomqtt](https://github.com/empicano/aiomqtt) from 2.3.2 to 2.4.0. - [Release
18+
notes](https://github.com/empicano/aiomqtt/releases) -
19+
[Changelog](https://github.com/empicano/aiomqtt/blob/main/CHANGELOG.md) -
20+
[Commits](https://github.com/empicano/aiomqtt/compare/v2.3.2...v2.4.0)
21+
22+
--- updated-dependencies: - dependency-name: aiomqtt dependency-version: 2.4.0
23+
24+
dependency-type: direct:production
25+
26+
update-type: version-update:semver-minor ...
27+
28+
Signed-off-by: dependabot[bot] <[email protected]>
29+
30+
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
31+
32+
### Features
33+
34+
- Add some logging for the web api
35+
([#377](https://github.com/Python-roborock/python-roborock/pull/377),
36+
[`74c1b5f`](https://github.com/Python-roborock/python-roborock/commit/74c1b5f6e88ce410f95676de802bd04d304963b1))
37+
38+
39+
## v2.18.2 (2025-05-04)
40+
41+
### Bug Fixes
42+
43+
- Add session to home_data_v3 ([#372](https://github.com/Python-roborock/python-roborock/pull/372),
44+
[`77061fe`](https://github.com/Python-roborock/python-roborock/commit/77061fe1545a3d2f9e874a3f7e4a94eedfd17706))
45+
46+
47+
## v2.18.1 (2025-05-04)
48+
49+
### Bug Fixes
50+
51+
- Get home_data_v3 working ([#371](https://github.com/Python-roborock/python-roborock/pull/371),
52+
[`f9e6c54`](https://github.com/Python-roborock/python-roborock/commit/f9e6c546e68a71a321dafabd5d502abef3e89b31))
53+
54+
455
## v2.18.0 (2025-04-06)
556

657
### Features

poetry.lock

Lines changed: 200 additions & 130 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-roborock"
3-
version = "2.18.0"
3+
version = "2.27.0"
44
description = "A package to control Roborock vacuums."
55
authors = ["humbertogontijo <[email protected]>"]
66
license = "GPL-3.0-only"
@@ -31,6 +31,7 @@ paho-mqtt = ">=1.6.1,<3.0.0"
3131
construct = "^2.10.57"
3232
vacuum-map-parser-roborock = "*"
3333
pyrate-limiter = "^3.7.0"
34+
aiomqtt = "^2.3.2"
3435

3536

3637
[build-system]
@@ -74,4 +75,5 @@ select=["E", "F", "UP", "I"]
7475
[tool.pytest.ini_options]
7576
asyncio_mode = "auto"
7677
asyncio_default_fixture_loop_scope = "function"
77-
timeout = 20
78+
timeout = 30
79+
log_format = "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s"

roborock/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def should_keepalive(self) -> bool:
8585

8686
async def validate_connection(self) -> None:
8787
if not self.should_keepalive():
88-
self._logger.info("Resetting Roborock connection due to kepalive timeout")
88+
self._logger.info("Resetting Roborock connection due to keepalive timeout")
8989
await self.async_disconnect()
9090
await self.async_connect()
9191

roborock/cli.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import json
45
import logging
56
from pathlib import Path
@@ -11,7 +12,8 @@
1112
from pyshark.packet.packet import Packet # type: ignore
1213

1314
from roborock import RoborockException
14-
from roborock.containers import DeviceData, HomeDataProduct, LoginData
15+
from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData
16+
from roborock.devices.device_manager import create_device_manager, create_home_data_api
1517
from roborock.protocol import MessageParser
1618
from roborock.util import run_sync
1719
from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
@@ -45,7 +47,8 @@ def validate(self):
4547
if self._login_data is None:
4648
raise RoborockException("You must login first")
4749

48-
def login_data(self):
50+
def login_data(self) -> LoginData:
51+
"""Get the login data."""
4952
self.validate()
5053
return self._login_data
5154

@@ -62,7 +65,11 @@ def cli(ctx, debug: int):
6265

6366
@click.command()
6467
@click.option("--email", required=True)
65-
@click.option("--password", required=True)
68+
@click.option(
69+
"--password",
70+
required=False,
71+
help="Password for the Roborock account. If not provided, an email code will be requested.",
72+
)
6673
@click.pass_context
6774
@run_sync()
6875
async def login(ctx, email, password):
@@ -75,10 +82,55 @@ async def login(ctx, email, password):
7582
except RoborockException:
7683
pass
7784
client = RoborockApiClient(email)
78-
user_data = await client.pass_login(password)
85+
if password is not None:
86+
user_data = await client.pass_login(password)
87+
else:
88+
print(f"Requesting code for {email}")
89+
await client.request_code()
90+
code = click.prompt("A code has been sent to your email, please enter the code", type=str)
91+
user_data = await client.code_login(code)
92+
print("Login successful")
7993
context.update(LoginData(user_data=user_data, email=email))
8094

8195

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+
home_data_api = create_home_data_api(login_data.email, login_data.user_data)
105+
106+
async def home_data_cache() -> HomeData:
107+
if login_data.home_data is None:
108+
login_data.home_data = await home_data_api()
109+
context.update(login_data)
110+
return login_data.home_data
111+
112+
# Create device manager
113+
device_manager = await create_device_manager(login_data.user_data, home_data_cache)
114+
115+
devices = await device_manager.get_devices()
116+
click.echo(f"Discovered devices: {', '.join([device.name for device in devices])}")
117+
118+
click.echo("MQTT session started. Querying devices...")
119+
for device in devices:
120+
try:
121+
status = await device.get_status()
122+
except RoborockException as e:
123+
click.echo(f"Failed to get status for {device.name}: {e}")
124+
else:
125+
click.echo(f"Device {device.name} status: {status.as_dict()}")
126+
127+
click.echo("Listening for messages.")
128+
await asyncio.sleep(duration)
129+
130+
# Close the device manager (this will close all devices and MQTT session)
131+
await device_manager.close()
132+
133+
82134
async def _discover(ctx):
83135
context: RoborockContext = ctx.obj
84136
login_data = context.login_data()
@@ -253,6 +305,7 @@ def on_package(packet: Packet):
253305
cli.add_command(status)
254306
cli.add_command(command)
255307
cli.add_command(parser)
308+
cli.add_command(session)
256309

257310

258311
def main():

roborock/cloud_api.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
from abc import ABC
77
from asyncio import Lock
88
from typing import Any
9-
from urllib.parse import urlparse
109

1110
import paho.mqtt.client as mqtt
1211

1312
from .api import KEEPALIVE, RoborockClient
1413
from .containers import DeviceData, UserData
1514
from .exceptions import RoborockException, VacuumError
16-
from .protocol import MessageParser, md5hex
15+
from .protocol import (
16+
Decoder,
17+
Encoder,
18+
create_mqtt_decoder,
19+
create_mqtt_encoder,
20+
create_mqtt_params,
21+
)
1722
from .roborock_future import RoborockFuture
1823

1924
_LOGGER = logging.getLogger(__name__)
@@ -53,27 +58,24 @@ def __init__(self, user_data: UserData, device_info: DeviceData) -> None:
5358
if rriot is None:
5459
raise RoborockException("Got no rriot data from user_data")
5560
RoborockClient.__init__(self, device_info)
61+
mqtt_params = create_mqtt_params(rriot)
5662
self._mqtt_user = rriot.u
57-
self._hashed_user = md5hex(self._mqtt_user + ":" + rriot.k)[2:10]
58-
url = urlparse(rriot.r.m)
59-
if not isinstance(url.hostname, str):
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
6466

6567
self._mqtt_client = _Mqtt()
6668
self._mqtt_client.on_connect = self._mqtt_on_connect
6769
self._mqtt_client.on_message = self._mqtt_on_message
6870
self._mqtt_client.on_disconnect = self._mqtt_on_disconnect
69-
if self._mqtt_ssl:
71+
if mqtt_params.tls:
7072
self._mqtt_client.tls_set()
7173

72-
self._mqtt_password = rriot.s
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)
7575
self._waiting_queue: dict[int, RoborockFuture] = {}
7676
self._mutex = Lock()
77+
self._decoder: Decoder = create_mqtt_decoder(device_info.device.local_key)
78+
self._encoder: Encoder = create_mqtt_encoder(device_info.device.local_key)
7779

7880
def _mqtt_on_connect(self, *args, **kwargs):
7981
_, __, ___, rc, ____ = args
@@ -102,7 +104,7 @@ def _mqtt_on_connect(self, *args, **kwargs):
102104
def _mqtt_on_message(self, *args, **kwargs):
103105
client, __, msg = args
104106
try:
105-
messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key)
107+
messages = self._decoder(msg.payload)
106108
super().on_message_received(messages)
107109
except Exception as ex:
108110
self._logger.exception(ex)

0 commit comments

Comments
 (0)