Skip to content

Commit c2587fd

Browse files
authored
Merge branch 'main' into dynamic_status_trait
2 parents e0dfd72 + 35f7910 commit c2587fd

22 files changed

+733
-96
lines changed

CHANGELOG.md

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

33
<!-- version list -->
44

5+
## v4.10.1 (2026-02-02)
6+
7+
### Bug Fixes
8+
9+
- Fix typo in B01_Q10_DP constant from REQUETDPS to REQUEST_DPS
10+
([#762](https://github.com/Python-roborock/python-roborock/pull/762),
11+
[`601a402`](https://github.com/Python-roborock/python-roborock/commit/601a4029fa975f43b6a04bfcd863dc2c8bd8b8ae))
12+
13+
- Fix typos in code mappings for Roborock
14+
([#762](https://github.com/Python-roborock/python-roborock/pull/762),
15+
[`601a402`](https://github.com/Python-roborock/python-roborock/commit/601a4029fa975f43b6a04bfcd863dc2c8bd8b8ae))
16+
17+
- Rename FUN_LEVEL to FAN_LEVEL in code mappings
18+
([#762](https://github.com/Python-roborock/python-roborock/pull/762),
19+
[`601a402`](https://github.com/Python-roborock/python-roborock/commit/601a4029fa975f43b6a04bfcd863dc2c8bd8b8ae))
20+
21+
- Typos in code mappings for Q10
22+
([#762](https://github.com/Python-roborock/python-roborock/pull/762),
23+
[`601a402`](https://github.com/Python-roborock/python-roborock/commit/601a4029fa975f43b6a04bfcd863dc2c8bd8b8ae))
24+
25+
### Chores
26+
27+
- Set typing for from_code_optional to use Self
28+
([#761](https://github.com/Python-roborock/python-roborock/pull/761),
29+
[`220ae8b`](https://github.com/Python-roborock/python-roborock/commit/220ae8bfc8b53d2e5070dc6c5211ef9b23df606d))
30+
31+
### Documentation
32+
33+
- Add comments documenting source code typos in B01_Q10_DP mappings
34+
([#762](https://github.com/Python-roborock/python-roborock/pull/762),
35+
[`601a402`](https://github.com/Python-roborock/python-roborock/commit/601a4029fa975f43b6a04bfcd863dc2c8bd8b8ae))
36+
37+
38+
## v4.10.0 (2026-02-01)
39+
40+
### Features
41+
42+
- Add clean record for Q7 ([#745](https://github.com/Python-roborock/python-roborock/pull/745),
43+
[`329e52b`](https://github.com/Python-roborock/python-roborock/commit/329e52bc34b1a5de2685b94002deae025eb0bd1c))
44+
45+
46+
## v4.9.1 (2026-02-01)
47+
48+
### Bug Fixes
49+
50+
- Correctly handle unknown categories
51+
([#755](https://github.com/Python-roborock/python-roborock/pull/755),
52+
[`742a382`](https://github.com/Python-roborock/python-roborock/commit/742a38200e943a987285cc6979c7e7d5ca729117))
53+
54+
55+
## v4.9.0 (2026-02-01)
56+
57+
### Features
58+
59+
- Add VacuumTrait to q10 devices
60+
([#754](https://github.com/Python-roborock/python-roborock/pull/754),
61+
[`69b6e0f`](https://github.com/Python-roborock/python-roborock/commit/69b6e0f58ce470f59a3d57756e6b4f760f3fd5a0))
62+
63+
564
## v4.8.0 (2026-01-27)
665

766
### Features

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 = "4.8.0"
3+
version = "4.10.1"
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/data/b01_q10/b01_q10_code_mappings.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class B01_Q10_DP(RoborockModeEnum):
3434
DUST_SETTING = ("dpDustSetting", 50)
3535
MAP_SAVE_SWITCH = ("dpMapSaveSwitch", 51)
3636
CLEAN_RECORD = ("dpCleanRecord", 52)
37-
RECEND_CLEAN_RECORD = ("dpRecendCleanRecord", 53)
37+
RECENT_CLEAN_RECORD = ("dpRecentCleanRecord", 53) # NOTE: typo "dpRecendCleanRecord" in source code
3838
RESTRICTED_ZONE = ("dpRestrictedZone", 54)
3939
RESTRICTED_ZONE_UP = ("dpRestrictedZoneUp", 55)
4040
VIRTUAL_WALL = ("dpVirtualWall", 56)
@@ -56,7 +56,7 @@ class B01_Q10_DP(RoborockModeEnum):
5656
ROOM_MERGE = ("dpRoomMerge", 72)
5757
ROOM_SPLIT = ("dpRoomSplit", 73)
5858
RESET_ROOM_NAME = ("dpResetRoomName", 74)
59-
REQUSET_NOT_DISTURB_DATA = ("dpRequsetNotDisturbData", 75)
59+
REQUEST_NOT_DISTURB_DATA = ("dpRequestNotDisturbData", 75) # NOTE: typo "dpRequsetNotDisturbData" in source code
6060
CARPET_CLEAN_TYPE = ("dpCarpetCleanType", 76)
6161
BUTTON_LIGHT_SWITCH = ("dpButtonLightSwitch", 77)
6262
CLEAN_LINE = ("dpCleanLine", 78)
@@ -68,7 +68,7 @@ class B01_Q10_DP(RoborockModeEnum):
6868
LOG_SWITCH = ("dpLogSwitch", 84)
6969
FLOOR_MATERIAL = ("dpFloorMaterial", 85)
7070
LINE_LASER_OBSTACLE_AVOIDANCE = ("dpLineLaserObstacleAvoidance", 86)
71-
CLEAN_PROGESS = ("dpCleanProgess", 87)
71+
CLEAN_PROGRESS = ("dpCleanProgress", 87) # NOTE: typo "dpCleanProgess" in source code
7272
GROUND_CLEAN = ("dpGroundClean", 88)
7373
IGNORE_OBSTACLE = ("dpIgnoreObstacle", 89)
7474
FAULT = ("dpFault", 90)
@@ -84,7 +84,7 @@ class B01_Q10_DP(RoborockModeEnum):
8484
SUSPECTED_THRESHOLD_UP = ("dpSuspectedThresholdUp", 100)
8585
COMMON = ("dpCommon", 101)
8686
JUMP_SCAN = ("dpJumpScan", 101)
87-
REQUETDPS = ("dpRequetdps", 102) # NOTE: THIS TYPO IS FOUND IN SOURCE CODE
87+
REQUEST_DPS = ("dpRequestDps", 102) # NOTE: typo "dpRequetdps" in source code
8888
CLIFF_RESTRICTED_AREA = ("dpCliffRestrictedArea", 102)
8989
CLIFF_RESTRICTED_AREA_UP = ("dpCliffRestrictedAreaUp", 103)
9090
BREAKPOINT_CLEAN = ("dpBreakpointClean", 104)
@@ -96,7 +96,7 @@ class B01_Q10_DP(RoborockModeEnum):
9696
HEARTBEAT = ("dpHeartbeat", 110)
9797
STATUS = ("dpStatus", 121)
9898
BATTERY = ("dpBattery", 122)
99-
FUN_LEVEL = ("dpfunLevel", 123)
99+
FAN_LEVEL = ("dpFanLevel", 123) # NOTE: typo "dpfunLevel" in source code
100100
WATER_LEVEL = ("dpWaterLevel", 124)
101101
MAIN_BRUSH_LIFE = ("dpMainBrushLife", 125)
102102
SIDE_BRUSH_LIFE = ("dpSideBrushLife", 126)
@@ -125,7 +125,7 @@ class YXFanLevel(RoborockModeEnum):
125125
NORMAL = "normal", 2
126126
STRONG = "strong", 3
127127
MAX = "max", 4
128-
SUPER = "super", 5
128+
SUPER = "super", 8
129129

130130

131131
class YXWaterLevel(RoborockModeEnum):

roborock/data/b01_q7/b01_q7_code_mappings.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,15 @@ class CleanTypeMapping(RoborockModeEnum):
4646
class CleanRepeatMapping(RoborockModeEnum):
4747
"""Maps the cleaning repeat parameter."""
4848

49-
ONCE = ("once", 0)
50-
TWICE = ("twice", 1)
49+
ONE = ("one", 0)
50+
TWO = ("two", 1)
51+
52+
53+
class CleanPathPreferenceMapping(RoborockModeEnum):
54+
"""Maps the cleaning path preference parameter."""
55+
56+
BALANCED = ("balanced", 0)
57+
DEEP = ("deep", 1)
5158

5259

5360
class SCDeviceCleanParam(RoborockModeEnum):

roborock/data/b01_q7/b01_q7_containers.py

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import datetime
2+
import json
13
from dataclasses import dataclass, field
4+
from functools import cached_property
25

6+
from ...exceptions import RoborockException
37
from ..containers import RoborockBase
48
from .b01_q7_code_mappings import (
59
B01Fault,
10+
CleanPathPreferenceMapping,
11+
CleanRepeatMapping,
612
CleanTypeMapping,
713
SCWindMapping,
814
WaterLevelMapping,
@@ -90,10 +96,10 @@ class B01Props(RoborockBase):
9096
mop_life: int | None = None
9197
main_sensor: int | None = None
9298
net_status: NetStatus | None = None
93-
repeat_state: int | None = None
99+
repeat_state: CleanRepeatMapping | None = None
94100
tank_state: int | None = None
95101
sweep_type: int | None = None
96-
clean_path_preference: int | None = None
102+
clean_path_preference: CleanPathPreferenceMapping | None = None
97103
cloth_state: int | None = None
98104
time_zone: int | None = None
99105
time_zone_info: str | None = None
@@ -205,3 +211,92 @@ def wind_name(self) -> str | None:
205211
def work_mode_name(self) -> str | None:
206212
"""Returns the name of the current work mode."""
207213
return self.work_mode.value if self.work_mode is not None else None
214+
215+
@property
216+
def repeat_state_name(self) -> str | None:
217+
"""Returns the name of the current repeat state."""
218+
return self.repeat_state.value if self.repeat_state is not None else None
219+
220+
@property
221+
def clean_path_preference_name(self) -> str | None:
222+
"""Returns the name of the current clean path preference."""
223+
return self.clean_path_preference.value if self.clean_path_preference is not None else None
224+
225+
226+
@dataclass
227+
class CleanRecordDetail(RoborockBase):
228+
"""Represents a single clean record detail (from `record_list[].detail`)."""
229+
230+
record_start_time: int | None = None
231+
method: int | None = None
232+
record_use_time: int | None = None
233+
clean_count: int | None = None
234+
# This is seemingly returned in meters (non-squared)
235+
record_clean_area: int | None = None
236+
record_clean_mode: int | None = None
237+
record_clean_way: int | None = None
238+
record_task_status: int | None = None
239+
record_faultcode: int | None = None
240+
record_dust_num: int | None = None
241+
clean_current_map: int | None = None
242+
record_map_url: str | None = None
243+
244+
@property
245+
def start_datetime(self) -> datetime.datetime | None:
246+
"""Convert the start datetime into a datetime object."""
247+
if self.record_start_time is not None:
248+
return datetime.datetime.fromtimestamp(self.record_start_time).astimezone(datetime.UTC)
249+
return None
250+
251+
@property
252+
def square_meters_area_cleaned(self) -> float | None:
253+
"""Returns the area cleaned in square meters."""
254+
if self.record_clean_area is not None:
255+
return self.record_clean_area / 100
256+
return None
257+
258+
259+
@dataclass
260+
class CleanRecordListItem(RoborockBase):
261+
"""Represents an entry in the clean record list returned by `service.get_record_list`."""
262+
263+
url: str | None = None
264+
detail: str | None = None
265+
266+
@cached_property
267+
def detail_parsed(self) -> CleanRecordDetail | None:
268+
"""Parse and return the detail as a CleanRecordDetail object."""
269+
if self.detail is None:
270+
return None
271+
try:
272+
parsed = json.loads(self.detail)
273+
except json.JSONDecodeError as ex:
274+
raise RoborockException(f"Invalid B01 record detail JSON: {self.detail!r}") from ex
275+
return CleanRecordDetail.from_dict(parsed)
276+
277+
278+
@dataclass
279+
class CleanRecordList(RoborockBase):
280+
"""Represents the clean record list response from `service.get_record_list`."""
281+
282+
total_area: int | None = None
283+
total_time: int | None = None # stored in seconds
284+
total_count: int | None = None
285+
record_list: list[CleanRecordListItem] = field(default_factory=list)
286+
287+
@property
288+
def square_meters_area_cleaned(self) -> float | None:
289+
"""Returns the area cleaned in square meters."""
290+
if self.total_area is not None:
291+
return self.total_area / 100
292+
return None
293+
294+
295+
@dataclass
296+
class CleanRecordSummary(RoborockBase):
297+
"""Represents clean record totals for B01/Q7 devices."""
298+
299+
total_time: int | None = None
300+
total_area: int | None = None
301+
total_count: int | None = None
302+
last_record_detail: CleanRecordDetail | None = None

roborock/data/code_mappings.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def from_code(cls, code: int) -> Self:
7777
raise ValueError(message)
7878

7979
@classmethod
80-
def from_code_optional(cls, code: int) -> RoborockModeEnum | None:
80+
def from_code_optional(cls, code: int) -> Self | None:
81+
"""Gracefully return None if the code does not exist."""
8182
try:
8283
return cls.from_code(code)
8384
except ValueError:
@@ -183,8 +184,10 @@ class RoborockCategory(Enum):
183184
WET_DRY_VAC = "roborock.wetdryvac"
184185
VACUUM = "robot.vacuum.cleaner"
185186
WASHING_MACHINE = "roborock.wm"
187+
MOWER = "roborock.mower"
186188
UNKNOWN = "UNKNOWN"
187189

188-
def __missing__(self, key):
189-
_LOGGER.warning("Missing key %s from category", key)
190+
@classmethod
191+
def _missing_(cls, value):
192+
_LOGGER.warning("Missing code %s from category", value)
190193
return RoborockCategory.UNKNOWN

roborock/devices/traits/b01/q10/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""Traits for Q10 B01 devices."""
22

3-
from typing import Any
4-
5-
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
63
from roborock.devices.traits import Trait
74
from roborock.devices.transport.mqtt_channel import MqttChannel
85

96
from .command import CommandTrait
7+
from .vacuum import VacuumTrait
108

119
__all__ = [
1210
"Q10PropertiesApi",
@@ -19,9 +17,13 @@ class Q10PropertiesApi(Trait):
1917
command: CommandTrait
2018
"""Trait for sending commands to Q10 devices."""
2119

20+
vacuum: VacuumTrait
21+
"""Trait for sending vacuum related commands to Q10 devices."""
22+
2223
def __init__(self, channel: MqttChannel) -> None:
2324
"""Initialize the B01Props API."""
2425
self.command = CommandTrait(channel)
26+
self.vacuum = VacuumTrait(self.command)
2527

2628

2729
def create(channel: MqttChannel) -> Q10PropertiesApi:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Traits for Q10 B01 devices."""
2+
3+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
4+
5+
from .command import CommandTrait
6+
7+
8+
class VacuumTrait:
9+
"""Trait for sending vacuum commands.
10+
11+
This is a wrapper around the CommandTrait for sending vacuum related
12+
commands to Q10 devices.
13+
"""
14+
15+
def __init__(self, command: CommandTrait) -> None:
16+
"""Initialize the VacuumTrait."""
17+
self._command = command
18+
19+
async def start_clean(self) -> None:
20+
"""Start cleaning."""
21+
await self._command.send(
22+
command=B01_Q10_DP.START_CLEAN,
23+
# TODO: figure out other commands
24+
# 1 = start cleaning
25+
# 2 = "electoral" clean, also has "clean_parameters"
26+
# 4 = fast create map
27+
params={"cmd": 1},
28+
)
29+
30+
async def pause_clean(self) -> None:
31+
"""Pause cleaning."""
32+
await self._command.send(
33+
command=B01_Q10_DP.PAUSE,
34+
params={},
35+
)
36+
37+
async def resume_clean(self) -> None:
38+
"""Resume cleaning."""
39+
await self._command.send(
40+
command=B01_Q10_DP.RESUME,
41+
params={},
42+
)
43+
44+
async def stop_clean(self) -> None:
45+
"""Stop cleaning."""
46+
await self._command.send(
47+
command=B01_Q10_DP.STOP,
48+
params={},
49+
)
50+
51+
async def return_to_dock(self) -> None:
52+
"""Return to dock."""
53+
await self._command.send(
54+
command=B01_Q10_DP.START_DOCK_TASK,
55+
params={},
56+
)

0 commit comments

Comments
 (0)