Skip to content

Commit e09e27f

Browse files
Merge branch 'main' into q10-map-layers
2 parents 8838626 + 031b9e2 commit e09e27f

28 files changed

Lines changed: 1143 additions & 303 deletions

CHANGELOG.md

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

33
<!-- version list -->
44

5+
## v5.31.1 (2026-07-18)
6+
7+
### Bug Fixes
8+
9+
- Correct reset consumable parameters for strainer and cleaning brush
10+
([#879](https://github.com/Python-roborock/python-roborock/pull/879),
11+
[`b11fc49`](https://github.com/Python-roborock/python-roborock/commit/b11fc4984445daa9a1837cef366d8543c3d1b7c8))
12+
13+
### Chores
14+
15+
- Add note about apache-II in CONTRIBUTING.md
16+
([#877](https://github.com/Python-roborock/python-roborock/pull/877),
17+
[`a8a5821`](https://github.com/Python-roborock/python-roborock/commit/a8a582192b3ec1c74f9b56c83bda026bd6ec194b))
18+
19+
20+
## v5.31.0 (2026-07-15)
21+
22+
### Features
23+
24+
- Add Q10 integration test and support stream streaming on FakeChannel
25+
([#869](https://github.com/Python-roborock/python-roborock/pull/869),
26+
[`502fa42`](https://github.com/Python-roborock/python-roborock/commit/502fa424c4a8fd36b759de8c8557c8665e4d0d13))
27+
28+
- Implement Q10VacuumSimulator and export it
29+
([#869](https://github.com/Python-roborock/python-roborock/pull/869),
30+
[`502fa42`](https://github.com/Python-roborock/python-roborock/commit/502fa424c4a8fd36b759de8c8557c8665e4d0d13))
31+
32+
- Implement stateful Q10 device simulator and integration tests
33+
([#869](https://github.com/Python-roborock/python-roborock/pull/869),
34+
[`502fa42`](https://github.com/Python-roborock/python-roborock/commit/502fa424c4a8fd36b759de8c8557c8665e4d0d13))
35+
36+
37+
## v5.30.0 (2026-07-13)
38+
39+
### Bug Fixes
40+
41+
- Gate dock_error_status on has_dock rather than is_collectable
42+
([#875](https://github.com/Python-roborock/python-roborock/pull/875),
43+
[`86acd87`](https://github.com/Python-roborock/python-roborock/commit/86acd874e74c391d85c63483bee02a88b8a2debb))
44+
45+
### Documentation
46+
47+
- Document metadata keys in field_metadata and is_field_supported
48+
([#875](https://github.com/Python-roborock/python-roborock/pull/875),
49+
[`86acd87`](https://github.com/Python-roborock/python-roborock/commit/86acd874e74c391d85c63483bee02a88b8a2debb))
50+
51+
### Features
52+
53+
- Implement device capability metadata for status computed property fields
54+
([#875](https://github.com/Python-roborock/python-roborock/pull/875),
55+
[`86acd87`](https://github.com/Python-roborock/python-roborock/commit/86acd874e74c391d85c63483bee02a88b8a2debb))
56+
57+
58+
## v5.29.0 (2026-07-12)
59+
60+
### Features
61+
62+
- Truncate large payloads in RoborockMessage __repr__
63+
([#873](https://github.com/Python-roborock/python-roborock/pull/873),
64+
[`95460d7`](https://github.com/Python-roborock/python-roborock/commit/95460d77a599613a0eddab3f4db7a36904f894d6))
65+
66+
67+
## v5.28.1 (2026-07-12)
68+
69+
### Bug Fixes
70+
71+
- Correct some dock mapping ([#872](https://github.com/Python-roborock/python-roborock/pull/872),
72+
[`9e95992`](https://github.com/Python-roborock/python-roborock/commit/9e95992fcfceb70a0e73c86dc819cecc6dc572d1))
73+
74+
### Refactoring
75+
76+
- Use stateful fakes and fixtures in integration tests
77+
([#868](https://github.com/Python-roborock/python-roborock/pull/868),
78+
[`89c8074`](https://github.com/Python-roborock/python-roborock/commit/89c80743e2c560f36f56e0c17c85c0d14544f406))
79+
80+
581
## v5.28.0 (2026-07-08)
682

783
### Features

CONTRIBUTING.md

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

33
Thank you for your interest in contributing to `python-roborock`! We welcome contributions from the community.
44

5+
## Licensing of Contributions
6+
7+
By submitting a contribution to this repository, you agree that your contribution is
8+
licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0),
9+
and you certify that you have the right to submit it under that license.
10+
11+
Note: the project is currently distributed under GPL-3.0 while we complete a planned
12+
migration to Apache 2.0. Apache 2.0 is GPL-compatible, so contributions made under
13+
Apache 2.0 can be included today and will carry over unchanged after the migration.
14+
515
## Getting Started
616

717
1. **Fork the repository** on GitHub.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-roborock"
3-
version = "5.28.0"
3+
version = "5.31.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"
@@ -52,6 +52,8 @@ dev = [
5252
# Pull in the CLI dependencies so maintainers can run the `roborock`
5353
# command and pdoc can import roborock.cli for docs generation.
5454
"python-roborock[cli]",
55+
# aioresponses is not compatible with aiohttp 3.14 yet. https://github.com/pnuckowski/aioresponses/issues/289
56+
"aiohttp>=3.8.2,<3.14",
5557
"pytest-asyncio>=1.1.0",
5658
"pytest",
5759
"pre-commit>=3.5,<5.0",

roborock/data/containers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ def _attr_repr(obj: Any) -> str:
6060
return f"{type(obj).__name__}({', '.join(parts)})"
6161

6262

63+
def field_metadata(**kwargs):
64+
"""Decorator to attach capability check metadata to a property.
65+
66+
This attaches a `_field_metadata` dictionary to the underlying getter function,
67+
which is then preserved when decorated with `@property`.
68+
69+
Supported metadata keys:
70+
- `feature` (str): Name of a capability property on `DeviceFeaturesTrait`.
71+
- `dock_feature` (str): Name of a capability property on `RoborockDockFeatures`.
72+
- `dps` (str/int): RoborockDataProtocol ID to check against supported schema IDs.
73+
"""
74+
75+
def decorator(func):
76+
func._field_metadata = kwargs
77+
return func
78+
79+
return decorator
80+
81+
6382
@dataclass(repr=False)
6483
class RoborockBase:
6584
"""Base class for all Roborock data classes."""

roborock/data/v1/v1_code_mappings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ class RoborockDockTypeCode(RoborockEnum):
675675
type_27_dock = 27
676676
k1c_lite_dock = 28
677677
shell_2e_lite_dock = 30
678+
shell_2e_heat_dock = 40
678679

679680

680681
class RoborockDockDustCollectionModeCode(RoborockEnum):

roborock/data/v1/v1_containers.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from roborock.exceptions import RoborockException
4040
from roborock.roborock_message import RoborockDataProtocol
4141

42-
from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr
42+
from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr, field_metadata
4343
from .v1_clean_modes import WashTowelModes
4444
from .v1_code_mappings import (
4545
CleanFluidStatus,
@@ -117,6 +117,15 @@ class StatusField(FieldNameBase):
117117
CHARGE_STATUS = "charge_status"
118118
DRY_STATUS = "dry_status"
119119
ERROR_CODE = "error_code"
120+
WATER_BOX_CARRIAGE_STATUS = "water_box_carriage_status"
121+
WATER_BOX_STATUS = "water_box_status"
122+
WATER_SHORTAGE_STATUS = "water_shortage_status"
123+
DIRTY_WATER_BOX_STATUS = "dirty_water_box_status"
124+
CLEAR_WATER_BOX_STATUS = "clear_water_box_status"
125+
CLEAN_FLUID_STATUS = "clean_fluid_status"
126+
CLEAN_PERCENT = "clean_percent"
127+
DOCK_ERROR_STATUS = "dock_error_status"
128+
RDT = "rdt"
120129

121130

122131
@dataclass
@@ -246,12 +255,14 @@ def has_am(self) -> bool | None:
246255
return (self.dss & 3) == 2
247256

248257
@property
258+
@field_metadata(dock_feature="is_washable")
249259
def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
250260
if self.dss:
251261
return ClearWaterBoxStatus((self.dss >> 2) & 3)
252262
return None
253263

254264
@property
265+
@field_metadata(dock_feature="is_washable")
255266
def dirty_water_box_status(self) -> DirtyWaterBoxStatus | None:
256267
if self.dss:
257268
return DirtyWaterBoxStatus((self.dss >> 4) & 3)
@@ -270,6 +281,9 @@ def water_box_filter_status(self) -> int | None:
270281
return None
271282

272283
@property
284+
@field_metadata(
285+
dock_feature="is_clean_fluid_auto_delivery_supported",
286+
)
273287
def clean_fluid_status(self) -> CleanFluidStatus | None:
274288
if self.dss:
275289
value = (self.dss >> 10) & 3
@@ -313,7 +327,7 @@ class StatusV2(RoborockBase):
313327
in_returning: int | None = None
314328
in_fresh_state: int | None = None
315329
lab_status: int | None = None
316-
water_box_status: int | None = None
330+
water_box_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
317331
back_type: int | None = None
318332
wash_phase: int | None = None
319333
wash_ready: int | None = None
@@ -323,14 +337,14 @@ class StatusV2(RoborockBase):
323337
is_locating: int | None = None
324338
lock_status: int | None = None
325339
water_box_mode: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE})
326-
water_box_carriage_status: int | None = None
340+
water_box_carriage_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
327341
mop_forbidden_enable: int | None = None
328342
camera_status: int | None = None
329343
is_exploring: int | None = None
330344
home_sec_status: int | None = None
331345
home_sec_enable_password: int | None = None
332346
adbumper_status: list[int] | None = None
333-
water_shortage_status: int | None = None
347+
water_shortage_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
334348
dock_type: RoborockDockTypeCode | None = None
335349
dust_collection_status: int | None = None
336350
auto_dust_collection: int | None = None
@@ -339,7 +353,8 @@ class StatusV2(RoborockBase):
339353
debug_mode: int | None = None
340354
collision_avoid_status: int | None = None
341355
switch_map_mode: int | None = None
342-
dock_error_status: RoborockDockErrorCode | None = None
356+
dock_error_status: RoborockDockErrorCode | None = field(default=None, metadata={"dock_feature": "has_dock"})
357+
343358
charge_status: RoborockChargeStatus | None = field(
344359
default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS}
345360
)
@@ -349,8 +364,8 @@ class StatusV2(RoborockBase):
349364
distance_off: int | None = None
350365
in_warmup: int | None = None
351366
dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
352-
rdt: int | None = None
353-
clean_percent: int | None = None
367+
rdt: int | None = field(default=None, metadata={"feature": "is_supported_drying"})
368+
clean_percent: int | None = field(default=None, metadata={"feature": "is_support_clean_estimate"})
354369
rss: int | None = None
355370
dss: int | None = None
356371
common_status: int | None = None
@@ -389,12 +404,14 @@ def has_am(self) -> bool | None:
389404
return (self.dss & 3) == 2
390405

391406
@property
407+
@field_metadata(dock_feature="is_washable")
392408
def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
393409
if self.dss:
394410
return ClearWaterBoxStatus((self.dss >> 2) & 3)
395411
return None
396412

397413
@property
414+
@field_metadata(dock_feature="is_washable")
398415
def dirty_water_box_status(self) -> DirtyWaterBoxStatus | None:
399416
if self.dss:
400417
return DirtyWaterBoxStatus((self.dss >> 4) & 3)
@@ -413,6 +430,9 @@ def water_box_filter_status(self) -> int | None:
413430
return None
414431

415432
@property
433+
@field_metadata(
434+
dock_feature="is_clean_fluid_auto_delivery_supported",
435+
)
416436
def clean_fluid_status(self) -> CleanFluidStatus | None:
417437
if self.dss:
418438
value = (self.dss >> 10) & 3

roborock/device_features.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,14 +674,14 @@ def get_supported_features(self) -> list[str]:
674674

675675
_CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES = {
676676
RoborockDockTypeCode.o4_dock,
677+
RoborockDockTypeCode.o5_dock,
677678
RoborockDockTypeCode.pearl_plus_dock,
678679
RoborockDockTypeCode.shell_3s_dock,
679680
RoborockDockTypeCode.o6_dock,
680681
RoborockDockTypeCode.couple_dock,
681682
}
682683

683684
_CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
684-
RoborockDockTypeCode.o5_dock,
685685
RoborockDockTypeCode.shell_2s_dock,
686686
RoborockDockTypeCode.shell_3_dock,
687687
RoborockDockTypeCode.shell_2c_dock,
@@ -734,9 +734,11 @@ def get_supported_features(self) -> list[str]:
734734
RoborockDockTypeCode.shell_2c_dock,
735735
RoborockDockTypeCode.shell_3s_dock,
736736
RoborockDockTypeCode.k1_dock,
737+
RoborockDockTypeCode.k1s_dock,
737738
RoborockDockTypeCode.o6_dock,
738739
RoborockDockTypeCode.shell_e_dock,
739740
RoborockDockTypeCode.shell_2e_dock,
741+
RoborockDockTypeCode.shell_3c_dock,
740742
}
741743

742744
_HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = {

roborock/devices/traits/v1/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ async def discover_features(self) -> None:
264264
# Dock type also acts like a device feature for some traits.
265265
dock_type = await self._dock_type()
266266
dock_features = RoborockDockFeatures.from_dock_type(dock_type, has_am=self.status.has_am)
267+
self.device_features.dock_features = dock_features
267268

268269
# Initialize traits with special arguments before the generic loop
269270
if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_features):

roborock/devices/traits/v1/consumeable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class ConsumableAttribute(StrEnum):
3030
FILTER_WORK_TIME = "filter_work_time"
3131
SIDE_BRUSH_WORK_TIME = "side_brush_work_time"
3232
MAIN_BRUSH_WORK_TIME = "main_brush_work_time"
33-
STRAINER_WORK_TIME = "strainer_work_time"
34-
CLEANING_BRUSH_WORK_TIME = "cleaning_brush_work_time"
33+
STRAINER_WORK_TIME = "strainer_work_times"
34+
CLEANING_BRUSH_WORK_TIME = "cleaning_brush_work_times"
3535

3636
@classmethod
3737
def from_str(cls, value: str) -> Self:

0 commit comments

Comments
 (0)