Skip to content

Commit cf998d0

Browse files
committed
Merge branch 'upstream/main' into decouple-b01-transport-layer
2 parents a52b328 + 8721d70 commit cf998d0

27 files changed

Lines changed: 1573 additions & 20 deletions

CHANGELOG.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,158 @@
22

33
<!-- version list -->
44

5+
## v5.25.0 (2026-07-03)
6+
7+
### Chores
8+
9+
- Add dataclasses for Roborock mower
10+
([#862](https://github.com/Python-roborock/python-roborock/pull/862),
11+
[`413d6e5`](https://github.com/Python-roborock/python-roborock/commit/413d6e57ae74d3d54f9f1d4c8918ffbe9d215b7b))
12+
13+
- Add some comments that better explain 'get_mop_only_vacuum_mode'
14+
([#817](https://github.com/Python-roborock/python-roborock/pull/817),
15+
[`9474bfa`](https://github.com/Python-roborock/python-roborock/commit/9474bfaa607f15c7b66f520af17d81c849651f68))
16+
17+
- Address comments ([#817](https://github.com/Python-roborock/python-roborock/pull/817),
18+
[`9474bfa`](https://github.com/Python-roborock/python-roborock/commit/9474bfaa607f15c7b66f520af17d81c849651f68))
19+
20+
- Address comments from copilot
21+
([#817](https://github.com/Python-roborock/python-roborock/pull/817),
22+
[`9474bfa`](https://github.com/Python-roborock/python-roborock/commit/9474bfaa607f15c7b66f520af17d81c849651f68))
23+
24+
- Apply suggestions from code review
25+
([#862](https://github.com/Python-roborock/python-roborock/pull/862),
26+
[`413d6e5`](https://github.com/Python-roborock/python-roborock/commit/413d6e57ae74d3d54f9f1d4c8918ffbe9d215b7b))
27+
28+
- Drop int typing and improve comment
29+
([#817](https://github.com/Python-roborock/python-roborock/pull/817),
30+
[`9474bfa`](https://github.com/Python-roborock/python-roborock/commit/9474bfaa607f15c7b66f520af17d81c849651f68))
31+
32+
### Features
33+
34+
- Add better cleaning mode support
35+
([#817](https://github.com/Python-roborock/python-roborock/pull/817),
36+
[`9474bfa`](https://github.com/Python-roborock/python-roborock/commit/9474bfaa607f15c7b66f520af17d81c849651f68))
37+
38+
39+
## v5.24.0 (2026-07-03)
40+
41+
### Features
42+
43+
- Implement dynamic map update listeners and filtering on HomeTrait
44+
([#861](https://github.com/Python-roborock/python-roborock/pull/861),
45+
[`a793c8e`](https://github.com/Python-roborock/python-roborock/commit/a793c8e0a3e0359027d491d406dd36f2d62d41f6))
46+
47+
48+
## v5.23.1 (2026-07-03)
49+
50+
### Bug Fixes
51+
52+
- Verify MQTT connection requires successful subscription
53+
([#858](https://github.com/Python-roborock/python-roborock/pull/858),
54+
[`e3c97c6`](https://github.com/Python-roborock/python-roborock/commit/e3c97c68ce5cf38022241bf85d68977c223b8922))
55+
56+
### Documentation
57+
58+
- Clarify is_mqtt_connected docstring with motivation
59+
([#858](https://github.com/Python-roborock/python-roborock/pull/858),
60+
[`e3c97c6`](https://github.com/Python-roborock/python-roborock/commit/e3c97c68ce5cf38022241bf85d68977c223b8922))
61+
62+
### Refactoring
63+
64+
- Remove unused fixtures in tests
65+
([#858](https://github.com/Python-roborock/python-roborock/pull/858),
66+
[`e3c97c6`](https://github.com/Python-roborock/python-roborock/commit/e3c97c68ce5cf38022241bf85d68977c223b8922))
67+
68+
69+
## v5.23.0 (2026-07-03)
70+
71+
### Features
72+
73+
- Add Q10 (B01/ss07) clean-record history trait
74+
([#857](https://github.com/Python-roborock/python-roborock/pull/857),
75+
[`79a996d`](https://github.com/Python-roborock/python-roborock/commit/79a996de463a59ff4de875579a417d157e8cc2e8))
76+
77+
- Q10 (B01/ss07) clean-record history trait
78+
([#857](https://github.com/Python-roborock/python-roborock/pull/857),
79+
[`79a996d`](https://github.com/Python-roborock/python-roborock/commit/79a996de463a59ff4de875579a417d157e8cc2e8))
80+
81+
### Refactoring
82+
83+
- Call the static parse_record via the class, not self
84+
([#857](https://github.com/Python-roborock/python-roborock/pull/857),
85+
[`79a996d`](https://github.com/Python-roborock/python-roborock/commit/79a996de463a59ff4de875579a417d157e8cc2e8))
86+
87+
- Widen parse_record's parameter type to Any | None
88+
([#857](https://github.com/Python-roborock/python-roborock/pull/857),
89+
[`79a996d`](https://github.com/Python-roborock/python-roborock/commit/79a996de463a59ff4de875579a417d157e8cc2e8))
90+
91+
92+
## v5.22.0 (2026-06-28)
93+
94+
### Bug Fixes
95+
96+
- Avoid Q10 Consumable/NetworkInfo shadowing v1 in roborock.data
97+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
98+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
99+
100+
- Correct Q10 vacuum command payloads, verified against ss07 hardware
101+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
102+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
103+
104+
- Keep Q10 CLIFF_RESTRICTED_AREA_UP (103); ss07 pushes it
105+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
106+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
107+
108+
- Show all Q10 read-model traits in status, wait for fresh push
109+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
110+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
111+
112+
- Stop unmapped Q10 data points from logging "not a valid code" warnings
113+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
114+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
115+
116+
### Features
117+
118+
- Add Q10 (B01/ss07) settings writers
119+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
120+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
121+
122+
- Add Q10 dust-collection frequency writer
123+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
124+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
125+
126+
- Decode Q10 add_clean_state as a bool
127+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
128+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
129+
130+
- Decode Q10 carpet/area/mop/floor-direction status into enums+bools
131+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
132+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
133+
134+
- Expand Q10 (B01/ss07) status support and add device info
135+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
136+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
137+
138+
- Q10 (B01/ss07) room/segment cleaning
139+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
140+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
141+
142+
- Q10 (B01/ss07) room/segment cleaning (clean_segments)
143+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
144+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
145+
146+
### Refactoring
147+
148+
- Split Q10 status/settings into per-concern traits
149+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
150+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
151+
152+
- Use YXDeviceCleanTask.ELECTORAL and inline misspelling note
153+
([#851](https://github.com/Python-roborock/python-roborock/pull/851),
154+
[`e71611b`](https://github.com/Python-roborock/python-roborock/commit/e71611b2b57f2ca1dc1d2c42d6fce0d3f1b6169b))
155+
156+
5157
## v5.21.0 (2026-06-25)
6158

7159
### Bug Fixes

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-roborock"
3-
version = "5.21.0"
3+
version = "5.25.0"
44
description = "A package to control Roborock vacuums."
55
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
66
requires-python = ">=3.11, <4"

roborock/cli.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,39 @@ async def q10_vacuum_spot(ctx: click.Context, device_id: str) -> None:
14821482
click.echo(f"Error: {e}")
14831483

14841484

1485+
@session.command()
1486+
@click.option("--device_id", required=True, help="Device ID")
1487+
@click.option(
1488+
"--segments",
1489+
required=True,
1490+
help="Comma-separated room/segment ids to clean (see the `rooms` command), e.g. 9,2",
1491+
)
1492+
@click.pass_context
1493+
@async_command
1494+
async def q10_clean_segments(ctx: click.Context, device_id: str, segments: str) -> None:
1495+
"""Start a room / segment clean on a Q10 device.
1496+
1497+
Room ids come from the `rooms` command (the device's map rooms).
1498+
"""
1499+
context: RoborockContext = ctx.obj
1500+
try:
1501+
segment_ids = [int(s) for s in segments.split(",") if s.strip()]
1502+
except ValueError:
1503+
click.echo("--segments must be comma-separated integers, e.g. 9,2")
1504+
return
1505+
if not segment_ids:
1506+
click.echo("No segment ids provided")
1507+
return
1508+
try:
1509+
trait = await _q10_vacuum_trait(context, device_id)
1510+
await trait.clean_segments(segment_ids)
1511+
click.echo(f"Starting room clean of segments {segment_ids}...")
1512+
except RoborockUnsupportedFeature:
1513+
click.echo("Device does not support B01 Q10 protocol. Is it a Q10?")
1514+
except RoborockException as e:
1515+
click.echo(f"Error: {e}")
1516+
1517+
14851518
async def _q10_set(ctx: click.Context, device_id: str, apply: Callable[[Any], Any], message: str) -> None:
14861519
"""Run a Q10 settings write and report the result."""
14871520
context: RoborockContext = ctx.obj

roborock/data/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
from .code_mappings import *
66
from .containers import *
77
from .dyad import *
8+
from .mower import *
89
from .v1 import *
910
from .zeo import *

roborock/data/b01_q10/b01_q10_code_mappings.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,43 @@ class YXDeviceCleanTask(RoborockModeEnum):
226226
PART = "part", 5
227227

228228

229+
class YXCleanScope(RoborockModeEnum):
230+
"""Clean scope/type as stored in a *clean record* (``dpCleanRecord``, field 7).
231+
232+
This is the same conceptual axis as the live :class:`YXDeviceCleanTask`, but the
233+
persisted record uses a different integer encoding -- e.g. a full clean records
234+
``0`` here vs ``1`` (``smart``) live, and a select-rooms clean records ``1`` here
235+
vs ``2`` (``electoral``) live. Ground-truthed against the app's History labels;
236+
code ``2`` was never observed on ss07 and is intentionally unmapped (so it
237+
resolves to ``None`` rather than a guessed label).
238+
"""
239+
240+
UNKNOWN = "unknown", -1
241+
FULL = "full", 0
242+
SELECTIVE_ROOM = "selective_room", 1
243+
ZONE = "zone", 3
244+
SPOT = "spot", 4
245+
246+
247+
class YXCleaningResult(RoborockModeEnum):
248+
"""How a clean ended, as stored in a clean record (``dpCleanRecord``, field 9)."""
249+
250+
UNKNOWN = "unknown", -1
251+
INTERRUPTED = "interrupted", 0 # ended on a fault
252+
COMPLETED = "completed", 1
253+
STOPPED = "stopped", 2 # ended early without a fault
254+
255+
256+
class YXStartMethod(RoborockModeEnum):
257+
"""What initiated a clean, as stored in a clean record (``dpCleanRecord``, field 10)."""
258+
259+
UNKNOWN = "unknown", -1
260+
REMOTE = "remote", 0
261+
APP = "app", 1
262+
TIMER = "timer", 2 # schedule / timer
263+
BUTTON = "button", 3 # device button
264+
265+
229266
class YXDeviceDustCollectionFrequency(RoborockModeEnum):
230267
# The app exposes "regular" (code 0) vs "frequent", where "frequent" selects
231268
# one of the every-N-cleans intervals below.

roborock/data/b01_q10/b01_q10_containers.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
automatically update objects from raw device responses.
77
"""
88

9+
import datetime
910
from dataclasses import dataclass, field
1011

1112
from ..containers import RoborockBase
@@ -14,12 +15,15 @@
1415
YXAreaUnit,
1516
YXBackType,
1617
YXCarpetCleanType,
18+
YXCleaningResult,
1719
YXCleanLine,
20+
YXCleanScope,
1821
YXCleanType,
1922
YXDeviceCleanTask,
2023
YXDeviceDustCollectionFrequency,
2124
YXDeviceState,
2225
YXFanLevel,
26+
YXStartMethod,
2327
YXWaterLevel,
2428
)
2529

@@ -32,6 +36,52 @@ class dpCleanRecord(RoborockBase):
3236
data: list
3337

3438

39+
@dataclass
40+
class Q10CleanRecord(RoborockBase):
41+
"""A single Q10 (ss07) clean record decoded from a ``dpCleanRecord`` (DP 52) entry.
42+
43+
The device returns each record as a 12-field underscore-delimited string in the
44+
``data`` list of a ``{"op": "list"}`` query (or the ``id`` of an ``{"op": "notify"}``
45+
push). The ``*_len`` values are internal blob-length metrics whose units aren't
46+
confirmed; the original ``raw`` string is always retained. The enum fields resolve
47+
an unmapped/unset code to ``None`` rather than guessing.
48+
"""
49+
50+
raw: str
51+
record_id: str | None = None
52+
start_time: int | None = None
53+
"""Clean start time, Unix seconds."""
54+
clean_time: int | None = None
55+
"""Cleaning time, minutes."""
56+
clean_area: int | None = None
57+
"""Cleaned area in square meters."""
58+
map_len: int | None = None
59+
"""Length of the saved map blob for this record (0 = none stored)."""
60+
path_len: int | None = None
61+
"""Length of the saved path blob for this record (0 = none stored)."""
62+
virtual_len: int | None = None
63+
"""Length of the saved virtual-restriction blob for this record (0 = none stored)."""
64+
clean_mode: YXCleanScope | None = None
65+
"""Clean scope/type (full / selective-room / zone / spot). Same axis as the live
66+
:class:`YXDeviceCleanTask` but a different record encoding -- see :class:`YXCleanScope`."""
67+
work_mode: YXCleanType | None = None
68+
"""Actual work performed (vac+mop / vacuum / mop) -- the same enum :class:`Q10Status`
69+
uses for the live clean-mode DP. Records only ever carry 1/2/3 here."""
70+
cleaning_result: YXCleaningResult | None = None
71+
"""How the clean ended: 0 interrupted (fault), 1 completed, 2 stopped (no fault)."""
72+
start_method: YXStartMethod | None = None
73+
"""What initiated the clean: 0 remote, 1 app, 2 timer, 3 button."""
74+
collect_dust_count: int | None = None
75+
"""Number of dock auto-empties during the clean."""
76+
77+
@property
78+
def start_datetime(self) -> datetime.datetime | None:
79+
"""The start time as a timezone-aware (UTC) datetime."""
80+
if self.start_time is not None:
81+
return datetime.datetime.fromtimestamp(self.start_time).astimezone(datetime.UTC)
82+
return None
83+
84+
3585
@dataclass
3686
class dpMultiMap(RoborockBase):
3787
op: str

roborock/data/mower/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Dataclasses for Roborock mower devices."""
2+
3+
from .mower_containers import *
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Data containers for Roborock mower devices."""
2+
3+
from dataclasses import dataclass, field
4+
from typing import Any
5+
6+
from roborock.data.containers import RoborockBase
7+
from roborock.roborock_message import RoborockMowerDataProtocol
8+
9+
10+
@dataclass
11+
class MowerStatus(RoborockBase):
12+
"""Core mower status backed by mower DPS updates."""
13+
14+
error_code: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE})
15+
battery: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BATTERY})
16+
mow_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE})
17+
mow_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE})
18+
mapping_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE})
19+
mapping_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_STATE})
20+
ota_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OTA_STATE})
21+
charge_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_STATE})
22+
dock_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.DOCK_STATE})
23+
charge_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_TYPE})
24+
pend_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.PEND_TYPE})
25+
remote_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.REMOTE_STATE})
26+
mow_start_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_START_TYPE})
27+
mow_eff_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE})
28+
mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT})
29+
mow_direction_angle: int | None = field(
30+
default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE}
31+
)
32+
mow_pattern: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN})
33+
mow_conf_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE})
34+
offline_status: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS})
35+
mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS})
36+
blade_lifespan: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BLADE_LIFESPAN})
37+
fc_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE})
38+
gps_coordinate: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE})
39+
off_dock_no_task_status: int | None = field(
40+
default=None, metadata={"dps": RoborockMowerDataProtocol.OFF_DOCK_NO_TASK_STATUS}
41+
)
42+
afs_status: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.AFS_STATUS})
43+
network_channel: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.NETWORK_CHANNEL})

0 commit comments

Comments
 (0)