Skip to content

Commit 8f75419

Browse files
authored
Merge branch 'main' into b01_sc
2 parents 8e2a06e + a80b306 commit 8f75419

32 files changed

+949
-248
lines changed

CHANGELOG.md

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

33
<!-- version list -->
44

5+
## v3.10.2 (2025-12-05)
6+
7+
### Bug Fixes
8+
9+
- Keep MQTT topic subscriptions alive with an idle timeout
10+
([#632](https://github.com/Python-roborock/python-roborock/pull/632),
11+
[`d0d2e42`](https://github.com/Python-roborock/python-roborock/commit/d0d2e425e3005f3f83f4a57079fcef4736171b7a))
12+
13+
### Chores
14+
15+
- Add tests that reproduce key parsing bugs
16+
([#631](https://github.com/Python-roborock/python-roborock/pull/631),
17+
[`87e14a2`](https://github.com/Python-roborock/python-roborock/commit/87e14a265a6c6bbe18fbe63f360ca57ca63db9c3))
18+
19+
- Fix lint errors ([#631](https://github.com/Python-roborock/python-roborock/pull/631),
20+
[`87e14a2`](https://github.com/Python-roborock/python-roborock/commit/87e14a265a6c6bbe18fbe63f360ca57ca63db9c3))
21+
22+
23+
## v3.10.1 (2025-12-05)
24+
25+
### Bug Fixes
26+
27+
- Add fallback ([#630](https://github.com/Python-roborock/python-roborock/pull/630),
28+
[`e4fa8c6`](https://github.com/Python-roborock/python-roborock/commit/e4fa8c60bb29978b06704ce22dc4a2cda0e28875))
29+
30+
- Ensure keys are correct type when serializing from data
31+
([#630](https://github.com/Python-roborock/python-roborock/pull/630),
32+
[`e4fa8c6`](https://github.com/Python-roborock/python-roborock/commit/e4fa8c60bb29978b06704ce22dc4a2cda0e28875))
33+
34+
- Ensure keys are valid type when serializing from data
35+
([#630](https://github.com/Python-roborock/python-roborock/pull/630),
36+
[`e4fa8c6`](https://github.com/Python-roborock/python-roborock/commit/e4fa8c60bb29978b06704ce22dc4a2cda0e28875))
37+
38+
39+
## v3.10.0 (2025-12-04)
40+
41+
### Bug Fixes
42+
43+
- Catch UnicodeDecodeError when parsing messages
44+
([#629](https://github.com/Python-roborock/python-roborock/pull/629),
45+
[`e8c3b75`](https://github.com/Python-roborock/python-roborock/commit/e8c3b75a9d3efb8ff79a6d4e8544549a5abe766a))
46+
47+
- Reset keep_alive_task to None
48+
([#627](https://github.com/Python-roborock/python-roborock/pull/627),
49+
[`a802f66`](https://github.com/Python-roborock/python-roborock/commit/a802f66fec913be82a25ae45d96555c2d328964b))
50+
51+
### Chores
52+
53+
- Copilot test ([#627](https://github.com/Python-roborock/python-roborock/pull/627),
54+
[`a802f66`](https://github.com/Python-roborock/python-roborock/commit/a802f66fec913be82a25ae45d96555c2d328964b))
55+
56+
### Features
57+
58+
- Add comprehensive test coverage for keep-alive functionality
59+
([#627](https://github.com/Python-roborock/python-roborock/pull/627),
60+
[`a802f66`](https://github.com/Python-roborock/python-roborock/commit/a802f66fec913be82a25ae45d96555c2d328964b))
61+
62+
- Add pinging to local client ([#627](https://github.com/Python-roborock/python-roborock/pull/627),
63+
[`a802f66`](https://github.com/Python-roborock/python-roborock/commit/a802f66fec913be82a25ae45d96555c2d328964b))
64+
65+
### Refactoring
66+
67+
- Address code review feedback on keep-alive tests
68+
([#627](https://github.com/Python-roborock/python-roborock/pull/627),
69+
[`a802f66`](https://github.com/Python-roborock/python-roborock/commit/a802f66fec913be82a25ae45d96555c2d328964b))
70+
71+
72+
## v3.9.3 (2025-12-03)
73+
74+
### Bug Fixes
75+
76+
- Use correct index for clean records
77+
([#620](https://github.com/Python-roborock/python-roborock/pull/620),
78+
[`f129603`](https://github.com/Python-roborock/python-roborock/commit/f1296032e7b8c8c1348882d58e9da5ecc8287eee))
79+
80+
81+
## v3.9.2 (2025-12-03)
82+
83+
### Bug Fixes
84+
85+
- Add device info getters and setters
86+
([#614](https://github.com/Python-roborock/python-roborock/pull/614),
87+
[`ee02a71`](https://github.com/Python-roborock/python-roborock/commit/ee02a71a8d99848256f2bb69533e9d1827f52585))
88+
89+
- Fix issues with the cache clobbering information for each device
90+
([#614](https://github.com/Python-roborock/python-roborock/pull/614),
91+
[`ee02a71`](https://github.com/Python-roborock/python-roborock/commit/ee02a71a8d99848256f2bb69533e9d1827f52585))
92+
93+
- Update DeviceCache interface ([#614](https://github.com/Python-roborock/python-roborock/pull/614),
94+
[`ee02a71`](https://github.com/Python-roborock/python-roborock/commit/ee02a71a8d99848256f2bb69533e9d1827f52585))
95+
96+
### Chores
97+
98+
- Fix test snapshots ([#614](https://github.com/Python-roborock/python-roborock/pull/614),
99+
[`ee02a71`](https://github.com/Python-roborock/python-roborock/commit/ee02a71a8d99848256f2bb69533e9d1827f52585))
100+
101+
- Remove unnecessary imports ([#614](https://github.com/Python-roborock/python-roborock/pull/614),
102+
[`ee02a71`](https://github.com/Python-roborock/python-roborock/commit/ee02a71a8d99848256f2bb69533e9d1827f52585))
103+
104+
105+
## v3.9.1 (2025-12-03)
106+
107+
### Bug Fixes
108+
109+
- Fix DeviceFeatures so that it can be serialized and deserialized properly.
110+
([#615](https://github.com/Python-roborock/python-roborock/pull/615),
111+
[`88b2055`](https://github.com/Python-roborock/python-roborock/commit/88b2055a7aea50d8b45bfb07c3a937b6d8d267d0))
112+
113+
114+
## v3.9.0 (2025-12-03)
115+
116+
### Bug Fixes
117+
118+
- Set default arugments to store/load value functions
119+
([#613](https://github.com/Python-roborock/python-roborock/pull/613),
120+
[`ce3d88d`](https://github.com/Python-roborock/python-roborock/commit/ce3d88dd52e78adccf7f705d4076cc963bbe9724))
121+
122+
### Chores
123+
124+
- Remove unncessary logging ([#613](https://github.com/Python-roborock/python-roborock/pull/613),
125+
[`ce3d88d`](https://github.com/Python-roborock/python-roborock/commit/ce3d88dd52e78adccf7f705d4076cc963bbe9724))
126+
127+
- Remove unnecessary snapshot files
128+
([#613](https://github.com/Python-roborock/python-roborock/pull/613),
129+
[`ce3d88d`](https://github.com/Python-roborock/python-roborock/commit/ce3d88dd52e78adccf7f705d4076cc963bbe9724))
130+
131+
- Remove unused import ([#613](https://github.com/Python-roborock/python-roborock/pull/613),
132+
[`ce3d88d`](https://github.com/Python-roborock/python-roborock/commit/ce3d88dd52e78adccf7f705d4076cc963bbe9724))
133+
134+
### Features
135+
136+
- Make CacheData serializable ([#613](https://github.com/Python-roborock/python-roborock/pull/613),
137+
[`ce3d88d`](https://github.com/Python-roborock/python-roborock/commit/ce3d88dd52e78adccf7f705d4076cc963bbe9724))
138+
139+
5140
## v3.8.5 (2025-11-29)
6141

7142
### Bug Fixes

SUPPORTED_FEATURES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
| `is_carpet_shape_type_supported` | | | |
3939
| `is_carpet_show_on_map` | | X | |
4040
| `is_carpet_supported` | X | X | |
41-
| `is_ces2022_supported` | | | |
41+
| `is_ces_2022_supported` | | | |
4242
| `is_clean_count_setting_supported` | | X | |
4343
| `is_clean_direct_status_supported` | | | |
4444
| `is_clean_efficiency_supported` | | | |

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 = "3.8.5"
3+
version = "3.10.2"
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: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from pyshark.packet.packet import Packet # type: ignore
4343

4444
from roborock import SHORT_MODEL_TO_ENUM, RoborockCommand
45-
from roborock.data import CombinedMapInfo, DeviceData, HomeData, NetworkInfo, RoborockBase, UserData
45+
from roborock.data import DeviceData, RoborockBase, UserData
4646
from roborock.device_features import DeviceFeatures
4747
from roborock.devices.cache import Cache, CacheData
4848
from roborock.devices.device import RoborockDevice
@@ -116,10 +116,8 @@ class ConnectionCache(RoborockBase):
116116

117117
user_data: UserData
118118
email: str
119-
home_data: HomeData | None = None
120-
network_info: dict[str, NetworkInfo] | None = None
121-
home_map_info: dict[int, CombinedMapInfo] | None = None
122-
trait_data: dict[str, Any] | None = None
119+
# TODO: Used new APIs for cache file storage
120+
cache_data: CacheData | None = None
123121

124122

125123
class DeviceConnectionManager:
@@ -134,10 +132,10 @@ def __init__(self, context: "RoborockContext", loop: asyncio.AbstractEventLoop |
134132
async def ensure_device_manager(self) -> DeviceManager:
135133
"""Ensure device manager is initialized."""
136134
if self.device_manager is None:
137-
cache_data = self.context.cache_data()
135+
connection_cache = self.context.connection_cache()
138136
user_params = UserParams(
139-
username=cache_data.email,
140-
user_data=cache_data.user_data,
137+
username=connection_cache.email,
138+
user_data=connection_cache.user_data,
141139
)
142140
self.device_manager = await create_device_manager(user_params, cache=self.context)
143141
# Cache devices for quick lookup
@@ -164,7 +162,8 @@ class RoborockContext(Cache):
164162
"""Context that handles both CLI and session modes internally."""
165163

166164
roborock_file = Path("~/.roborock").expanduser()
167-
_cache_data: ConnectionCache | None = None
165+
roborock_cache_file = Path("~/.roborock.cache").expanduser()
166+
_connection_cache: ConnectionCache | None = None
168167

169168
def __init__(self):
170169
self.reload()
@@ -177,22 +176,22 @@ def reload(self):
177176
with open(self.roborock_file) as f:
178177
data = json.load(f)
179178
if data:
180-
self._cache_data = ConnectionCache.from_dict(data)
179+
self._connection_cache = ConnectionCache.from_dict(data)
181180

182-
def update(self, cache_data: ConnectionCache):
183-
data = json.dumps(cache_data.as_dict(), default=vars, indent=4)
181+
def update(self, connection_cache: ConnectionCache):
182+
data = json.dumps(connection_cache.as_dict(), default=vars, indent=4)
184183
with open(self.roborock_file, "w") as f:
185184
f.write(data)
186185
self.reload()
187186

188187
def validate(self):
189-
if self._cache_data is None:
188+
if self._connection_cache is None:
190189
raise RoborockException("You must login first")
191190

192-
def cache_data(self) -> ConnectionCache:
191+
def connection_cache(self) -> ConnectionCache:
193192
"""Get the cache data."""
194193
self.validate()
195-
return cast(ConnectionCache, self._cache_data)
194+
return cast(ConnectionCache, self._connection_cache)
196195

197196
def start_session_mode(self):
198197
"""Start session mode with a background event loop."""
@@ -229,19 +228,21 @@ async def get_device_manager(self) -> DeviceConnectionManager:
229228

230229
async def refresh_devices(self) -> ConnectionCache:
231230
"""Refresh device data from server (always fetches fresh data)."""
232-
cache_data = self.cache_data()
233-
client = RoborockApiClient(cache_data.email)
234-
home_data = await client.get_home_data_v3(cache_data.user_data)
235-
cache_data.home_data = home_data
236-
self.update(cache_data)
237-
return cache_data
231+
connection_cache = self.connection_cache()
232+
client = RoborockApiClient(connection_cache.email)
233+
home_data = await client.get_home_data_v3(connection_cache.user_data)
234+
if connection_cache.cache_data is None:
235+
connection_cache.cache_data = CacheData()
236+
connection_cache.cache_data.home_data = home_data
237+
self.update(connection_cache)
238+
return connection_cache
238239

239240
async def get_devices(self) -> ConnectionCache:
240241
"""Get device data (uses cache if available, fetches if needed)."""
241-
cache_data = self.cache_data()
242-
if not cache_data.home_data:
243-
cache_data = await self.refresh_devices()
244-
return cache_data
242+
connection_cache = self.connection_cache()
243+
if (connection_cache.cache_data is None) or (connection_cache.cache_data.home_data is None):
244+
connection_cache = await self.refresh_devices()
245+
return connection_cache
245246

246247
async def cleanup(self):
247248
"""Clean up resources (mainly for session mode)."""
@@ -266,22 +267,16 @@ def finish_session(self) -> None:
266267
async def get(self) -> CacheData:
267268
"""Get cached value."""
268269
_LOGGER.debug("Getting cache data")
269-
connection_cache = self.cache_data()
270-
return CacheData(
271-
home_data=connection_cache.home_data,
272-
network_info=connection_cache.network_info or {},
273-
home_map_info=connection_cache.home_map_info,
274-
trait_data=connection_cache.trait_data or {},
275-
)
270+
connection_cache = self.connection_cache()
271+
if connection_cache.cache_data is not None:
272+
return connection_cache.cache_data
273+
return CacheData()
276274

277275
async def set(self, value: CacheData) -> None:
278276
"""Set value in the cache."""
279277
_LOGGER.debug("Setting cache data")
280-
connection_cache = self.cache_data()
281-
connection_cache.home_data = value.home_data
282-
connection_cache.network_info = value.network_info
283-
connection_cache.home_map_info = value.home_map_info
284-
connection_cache.trait_data = value.trait_data
278+
connection_cache = self.connection_cache()
279+
connection_cache.cache_data = value
285280
self.update(connection_cache)
286281

287282

@@ -367,9 +362,9 @@ async def discover(ctx):
367362
@async_command
368363
async def list_devices(ctx):
369364
context: RoborockContext = ctx.obj
370-
cache_data = await context.get_devices()
365+
connection_cache = await context.get_devices()
371366

372-
home_data = cache_data.home_data
367+
home_data = connection_cache.cache_data.home_data
373368

374369
device_name_id = {device.name: device.duid for device in home_data.get_all_devices()}
375370
click.echo(json.dumps(device_name_id, indent=4))

roborock/data/containers.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dataclasses
22
import datetime
3+
import inspect
34
import json
45
import logging
56
import re
@@ -27,7 +28,11 @@ def _camelize(s: str):
2728

2829

2930
def _decamelize(s: str):
30-
return re.sub("([A-Z]+)", "_\\1", s).lower()
31+
# Split before uppercase letters not at the start, and before numbers
32+
s = re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", s)
33+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s) # Split acronyms followed by normal camelCase
34+
s = re.sub(r"([a-zA-Z])([0-9]+)", r"\1_\2", s)
35+
return s.lower()
3136

3237

3338
def _attr_repr(obj: Any) -> str:
@@ -62,13 +67,16 @@ def _convert_to_class_obj(class_type: type, value):
6267
sub_type = get_args(class_type)[0]
6368
return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value]
6469
if get_origin(class_type) is dict:
65-
_, value_type = get_args(class_type) # assume keys are only basic types
70+
key_type, value_type = get_args(class_type)
71+
if key_type is not None:
72+
return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
6673
return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
67-
if issubclass(class_type, RoborockBase):
68-
return class_type.from_dict(value)
69-
if issubclass(class_type, RoborockModeEnum):
70-
return class_type.from_code(value)
71-
if class_type is Any:
74+
if inspect.isclass(class_type):
75+
if issubclass(class_type, RoborockBase):
76+
return class_type.from_dict(value)
77+
if issubclass(class_type, RoborockModeEnum):
78+
return class_type.from_code(value)
79+
if class_type is Any or type(class_type) is str:
7280
return value
7381
return class_type(value) # type: ignore[call-arg]
7482

roborock/device_features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class DeviceFeatures(RoborockBase):
313313
is_support_incremental_map: bool = field(metadata={"new_feature_str_mask": (4194304, 8)})
314314
is_offline_map_supported: bool = field(metadata={"new_feature_str_mask": (16384, 8)})
315315
is_super_deep_wash_supported: bool = field(metadata={"new_feature_str_mask": (32768, 8)})
316-
is_ces2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
316+
is_ces_2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
317317
is_dss_believable: bool = field(metadata={"new_feature_str_mask": (131072, 8)})
318318
is_main_brush_up_down_supported_from_str: bool = field(metadata={"new_feature_str_mask": (262144, 8)})
319319
is_goto_pure_clean_path_supported: bool = field(metadata={"new_feature_str_mask": (524288, 8)})

0 commit comments

Comments
 (0)