Skip to content

Commit 13170e2

Browse files
authored
Merge branch 'main' into leo/q7-map-content-followup
2 parents 1010ddb + a36a956 commit 13170e2

30 files changed

+428
-244
lines changed

CHANGELOG.md

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

33
<!-- version list -->
44

5+
## v4.25.0 (2026-03-16)
6+
7+
### Chores
8+
9+
- Apply suggestions from code review
10+
([#788](https://github.com/Python-roborock/python-roborock/pull/788),
11+
[`19d7674`](https://github.com/Python-roborock/python-roborock/commit/19d7674cbf98dcf1ba591d1bf71f87b370a90a55))
12+
13+
### Features
14+
15+
- Add `from_any_optional` method to `CodeMapping` for flexible enum resolution with corresponding
16+
tests. ([#788](https://github.com/Python-roborock/python-roborock/pull/788),
17+
[`19d7674`](https://github.com/Python-roborock/python-roborock/commit/19d7674cbf98dcf1ba591d1bf71f87b370a90a55))
18+
19+
- Add `from_any_optional` method to `RoborockModeEnum`
20+
([#788](https://github.com/Python-roborock/python-roborock/pull/788),
21+
[`19d7674`](https://github.com/Python-roborock/python-roborock/commit/19d7674cbf98dcf1ba591d1bf71f87b370a90a55))
22+
23+
### Refactoring
24+
25+
- Simplify B01_Q10 command parsing by removing a helper function and utilizing `from_any_optional`.
26+
([#788](https://github.com/Python-roborock/python-roborock/pull/788),
27+
[`19d7674`](https://github.com/Python-roborock/python-roborock/commit/19d7674cbf98dcf1ba591d1bf71f87b370a90a55))
28+
29+
30+
## v4.24.0 (2026-03-16)
31+
32+
### Chores
33+
34+
- Fix lint. ([#787](https://github.com/Python-roborock/python-roborock/pull/787),
35+
[`08ca9aa`](https://github.com/Python-roborock/python-roborock/commit/08ca9aa2f9dfb85f41e427d62c3ef189d3a48727))
36+
37+
- Fix MAX_PLUS enum value ([#787](https://github.com/Python-roborock/python-roborock/pull/787),
38+
[`08ca9aa`](https://github.com/Python-roborock/python-roborock/commit/08ca9aa2f9dfb85f41e427d62c3ef189d3a48727))
39+
40+
- Rename and reorder `YXFanLevel` enum members
41+
([#787](https://github.com/Python-roborock/python-roborock/pull/787),
42+
[`08ca9aa`](https://github.com/Python-roborock/python-roborock/commit/08ca9aa2f9dfb85f41e427d62c3ef189d3a48727))
43+
44+
### Documentation
45+
46+
- Add docstring and alias comments to the YXFanLevel enum.
47+
([#787](https://github.com/Python-roborock/python-roborock/pull/787),
48+
[`08ca9aa`](https://github.com/Python-roborock/python-roborock/commit/08ca9aa2f9dfb85f41e427d62c3ef189d3a48727))
49+
50+
### Features
51+
52+
- Rename and reorder `YXFanLevel` enum members
53+
([#787](https://github.com/Python-roborock/python-roborock/pull/787),
54+
[`08ca9aa`](https://github.com/Python-roborock/python-roborock/commit/08ca9aa2f9dfb85f41e427d62c3ef189d3a48727))
55+
56+
57+
## v4.23.0 (2026-03-16)
58+
59+
### Chores
60+
61+
- Remove duplicate V1TraitDataConverter
62+
([#783](https://github.com/Python-roborock/python-roborock/pull/783),
63+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
64+
65+
- Remove unused `typing.Self` import.
66+
([#783](https://github.com/Python-roborock/python-roborock/pull/783),
67+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
68+
69+
### Documentation
70+
71+
- Clarify internal usage of V1TraitDataConverter and V1TraitMixin attributes.
72+
([#783](https://github.com/Python-roborock/python-roborock/pull/783),
73+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
74+
75+
### Features
76+
77+
- Separate trait response handling logic from refresh logic and merge
78+
([#783](https://github.com/Python-roborock/python-roborock/pull/783),
79+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
80+
81+
- Simplify V1 trait handling ([#783](https://github.com/Python-roborock/python-roborock/pull/783),
82+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
83+
84+
### Refactoring
85+
86+
- Make V1TraitDataConverter an abstract base class, use a dedicated LedStatusConverter, and fix a
87+
typo in Rooms. ([#783](https://github.com/Python-roborock/python-roborock/pull/783),
88+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
89+
90+
- Remove trait update listeners and centralize data conversion into dedicated converter classes
91+
([#783](https://github.com/Python-roborock/python-roborock/pull/783),
92+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
93+
94+
- Standardize trait data merging to `merge_trait_values` and remove direct `_parse_response` methods
95+
from traits. ([#783](https://github.com/Python-roborock/python-roborock/pull/783),
96+
[`9f9c1b4`](https://github.com/Python-roborock/python-roborock/commit/9f9c1b4b9271a6a63a0dbe6afd21216b13a15648))
97+
98+
599
## v4.22.0 (2026-03-14)
6100

7101
### 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.22.0"
3+
version = "4.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: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -764,21 +764,6 @@ async def network_info(ctx, device_id: str):
764764
await _display_v1_trait(context, device_id, lambda v1: v1.network_info)
765765

766766

767-
def _parse_b01_q10_command(cmd: str) -> B01_Q10_DP:
768-
"""Parse B01_Q10 command from either enum name or value."""
769-
try:
770-
return B01_Q10_DP(int(cmd))
771-
except ValueError:
772-
try:
773-
return B01_Q10_DP.from_name(cmd)
774-
except ValueError:
775-
try:
776-
return B01_Q10_DP.from_value(cmd)
777-
except ValueError:
778-
pass
779-
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")
780-
781-
782767
@click.command()
783768
@click.option("--device_id", required=True)
784769
@click.option("--cmd", required=True)
@@ -795,7 +780,8 @@ async def command(ctx, cmd, device_id, params):
795780
if result:
796781
click.echo(dump_json(result))
797782
elif device.b01_q10_properties is not None:
798-
cmd_value = _parse_b01_q10_command(cmd)
783+
if cmd_value := B01_Q10_DP.from_any_optional(cmd) is None:
784+
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")
799785
command_trait: Trait = device.b01_q10_properties.command
800786
await command_trait.send(cmd_value, json.loads(params) if params is not None else None)
801787
click.echo("Command sent successfully; Enable debug logging (-d) to see responses.")

roborock/data/b01_q10/b01_q10_code_mappings.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,19 @@ class B01_Q10_DP(RoborockModeEnum):
119119

120120

121121
class YXFanLevel(RoborockModeEnum):
122+
"""The fan or vacuum level of the robot.
123+
124+
Note: The names used here are the v1 names, though the values
125+
have different aliases in the app bundles.
126+
"""
127+
122128
UNKNOWN = "unknown", -1
123-
CLOSE = "close", 0
129+
OFF = "off", 0 # close
124130
QUIET = "quiet", 1
125-
NORMAL = "normal", 2
126-
STRONG = "strong", 3
131+
BALANCED = "balanced", 2 # normal
132+
TURBO = "turbo", 3 # strong
127133
MAX = "max", 4
128-
SUPER = "super", 8
134+
MAX_PLUS = "max_plus", 8 # super
129135

130136

131137
class YXWaterLevel(RoborockModeEnum):

roborock/data/code_mappings.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ def from_name(cls, name: str) -> Self:
100100
return member
101101
raise ValueError(f"{name} is not a valid name for {cls.__name__}")
102102

103+
@classmethod
104+
def from_any_optional(cls, value: str | int) -> Self | None:
105+
"""Resolve a string or int to an enum member.
106+
107+
Tries to look up by enum name, string value, or integer code
108+
and returns None if no match is found.
109+
"""
110+
# Try enum name lookup (e.g. "SEEK")
111+
try:
112+
return cls.from_name(str(value))
113+
except ValueError:
114+
pass
115+
# Try DP string value lookup (e.g. "dpSeek")
116+
try:
117+
return cls.from_value(str(value))
118+
except ValueError:
119+
pass
120+
# Try integer code lookup (e.g. "11")
121+
try:
122+
return cls.from_code(int(value))
123+
except (ValueError, TypeError):
124+
pass
125+
return None
126+
103127
@classmethod
104128
def keys(cls) -> list[str]:
105129
"""Returns a list of all member values."""

roborock/devices/traits/v1/child_lock.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class ChildLockTrait(ChildLockStatus, common.V1TraitMixin, common.RoborockSwitch
99
"""Trait for controlling the child lock of a Roborock device."""
1010

1111
command = RoborockCommand.GET_CHILD_LOCK_STATUS
12+
converter = common.DefaultConverter(ChildLockStatus)
1213
requires_feature = "is_set_child_supported"
1314

1415
@property
Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,37 @@
11
import logging
2-
from typing import Self
32

4-
from roborock.data import CleanRecord, CleanSummaryWithDetail
3+
from roborock.data import CleanRecord, CleanSummaryWithDetail, RoborockBase
54
from roborock.devices.traits.v1 import common
65
from roborock.roborock_typing import RoborockCommand
76
from roborock.util import unpack_list
87

98
_LOGGER = logging.getLogger(__name__)
109

1110

12-
class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
13-
"""Trait for managing the clean summary of Roborock devices."""
14-
15-
command = RoborockCommand.GET_CLEAN_SUMMARY
16-
17-
async def refresh(self) -> None:
18-
"""Refresh the clean summary data and last clean record.
19-
20-
Assumes that the clean summary has already been fetched.
21-
"""
22-
await super().refresh()
23-
if not self.records:
24-
_LOGGER.debug("No clean records available in clean summary.")
25-
self.last_clean_record = None
26-
return
27-
last_record_id = self.records[0]
28-
self.last_clean_record = await self.get_clean_record(last_record_id)
11+
class CleanSummaryConverter(common.V1TraitDataConverter):
12+
"""Converter for CleanSummaryWithDetail objects."""
2913

30-
@classmethod
31-
def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
14+
def convert(self, response: common.V1ResponseData) -> RoborockBase:
3215
"""Parse the response from the device into a CleanSummary."""
3316
if isinstance(response, dict):
34-
return cls.from_dict(response)
17+
return CleanSummaryWithDetail.from_dict(response)
3518
elif isinstance(response, list):
3619
clean_time, clean_area, clean_count, records = unpack_list(response, 4)
37-
return cls(
20+
return CleanSummaryWithDetail(
3821
clean_time=clean_time,
3922
clean_area=clean_area,
4023
clean_count=clean_count,
4124
records=records,
4225
)
4326
elif isinstance(response, int):
44-
return cls(clean_time=response)
27+
return CleanSummaryWithDetail(clean_time=response)
4528
raise ValueError(f"Unexpected clean summary format: {response!r}")
4629

47-
async def get_clean_record(self, record_id: int) -> CleanRecord:
48-
"""Load a specific clean record by ID."""
49-
response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
50-
return self._parse_clean_record_response(response)
5130

52-
@classmethod
53-
def _parse_clean_record_response(cls, response: common.V1ResponseData) -> CleanRecord:
31+
class CleanRecordConverter(common.V1TraitDataConverter):
32+
"""Convert server responses to a CleanRecord."""
33+
34+
def convert(self, response: common.V1ResponseData) -> CleanRecord:
5435
"""Parse the response from the device into a CleanRecord."""
5536
if isinstance(response, list) and len(response) == 1:
5637
response = response[0]
@@ -81,3 +62,29 @@ def _parse_clean_record_response(cls, response: common.V1ResponseData) -> CleanR
8162
begin, end, duration, area = unpack_list(response, 4)
8263
return CleanRecord(begin=begin, end=end, duration=duration, area=area)
8364
raise ValueError(f"Unexpected clean record format: {response!r}")
65+
66+
67+
class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
68+
"""Trait for managing the clean summary of Roborock devices."""
69+
70+
command = RoborockCommand.GET_CLEAN_SUMMARY
71+
converter = CleanSummaryConverter()
72+
clean_record_converter = CleanRecordConverter()
73+
74+
async def refresh(self) -> None:
75+
"""Refresh the clean summary data and last clean record.
76+
77+
Assumes that the clean summary has already been fetched.
78+
"""
79+
await super().refresh()
80+
if not self.records:
81+
_LOGGER.debug("No clean records available in clean summary.")
82+
self.last_clean_record = None
83+
return
84+
last_record_id = self.records[0]
85+
self.last_clean_record = await self.get_clean_record(last_record_id)
86+
87+
async def get_clean_record(self, record_id: int) -> CleanRecord:
88+
"""Load a specific clean record by ID."""
89+
response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
90+
return self.clean_record_converter.convert(response)

0 commit comments

Comments
 (0)