Skip to content

Commit 437ec29

Browse files
authored
Merge branch 'main' into pr/zeo-core-api-v2
2 parents 239d779 + bfc23b5 commit 437ec29

22 files changed

Lines changed: 761 additions & 14 deletions

CHANGELOG.md

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

33
<!-- version list -->
44

5+
## v6.0.0 (2026-07-27)
6+
7+
### Features
8+
9+
- Audit and complete Zeo protocol DP and enum mappings
10+
([#892](https://github.com/Python-roborock/python-roborock/pull/892),
11+
[`dba564d`](https://github.com/Python-roborock/python-roborock/commit/dba564d10b88afcd6195298b90c6d49844e334f5))
12+
13+
- Full Zeo protocol definition — 67 DPs, complete enum mappings, all 56 devices covered
14+
([#892](https://github.com/Python-roborock/python-roborock/pull/892),
15+
[`dba564d`](https://github.com/Python-roborock/python-roborock/commit/dba564d10b88afcd6195298b90c6d49844e334f5))
16+
17+
### Refactoring
18+
19+
- **zeo**: Replace raw integer fields with enum types
20+
([#892](https://github.com/Python-roborock/python-roborock/pull/892),
21+
[`dba564d`](https://github.com/Python-roborock/python-roborock/commit/dba564d10b88afcd6195298b90c6d49844e334f5))
22+
23+
24+
## v5.37.1 (2026-07-25)
25+
26+
### Bug Fixes
27+
28+
- Add missing dock ids
29+
([`f431c5d`](https://github.com/Python-roborock/python-roborock/commit/f431c5dbab2730df73fdaf5b6c42319a0a77a07d))
30+
31+
32+
## v5.37.0 (2026-07-25)
33+
34+
### Chores
35+
36+
- Clean up security location ([#880](https://github.com/Python-roborock/python-roborock/pull/880),
37+
[`f0579b8`](https://github.com/Python-roborock/python-roborock/commit/f0579b8dcee5be26da0c572d686f3a831d3867a9))
38+
39+
- Mild clean ups ([#880](https://github.com/Python-roborock/python-roborock/pull/880),
40+
[`f0579b8`](https://github.com/Python-roborock/python-roborock/commit/f0579b8dcee5be26da0c572d686f3a831d3867a9))
41+
42+
### Features
43+
44+
- Add photo for obstacle avoidance
45+
([#880](https://github.com/Python-roborock/python-roborock/pull/880),
46+
[`f0579b8`](https://github.com/Python-roborock/python-roborock/commit/f0579b8dcee5be26da0c572d686f3a831d3867a9))
47+
48+
549
## v5.36.0 (2026-07-21)
650

751
### 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 = "5.36.0"
3+
version = "6.0.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/data/v1/v1_code_mappings.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,10 +365,27 @@ class RoborockDockTypeCode(RoborockEnum):
365365
shell_e_dock = 21
366366
shell_2e_dock = 22
367367
shell_3c_dock = 23
368+
hera_dock = 24
369+
k1s_pro_dock = 26
368370
type_27_dock = 27
369371
k1c_lite_dock = 28
372+
k1r_dock = 29
370373
shell_2e_lite_dock = 30
374+
shell_4rc_dock = 31
375+
shell_4r_dock = 32
376+
shell_4p_dock = 33
377+
shell_4s_dock = 34
378+
o7_dock = 35
379+
k1d_dock = 37
380+
shell_3p_dock = 38
381+
shell_4d_dock = 39
371382
shell_2e_heat_dock = 40
383+
f1_dock = 41
384+
f1r_dock = 42
385+
f2r_dock = 43
386+
f1s_dock = 44
387+
o7h_dock = 45
388+
f1c_dock = 46
372389

373390

374391
class RoborockDockDustCollectionModeCode(RoborockEnum):

roborock/device_features.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,12 @@ class DeviceFeatures(RoborockBase):
539539
]
540540
}
541541
)
542+
is_ai_recognition_setting_supported: bool = field(
543+
metadata={"product_features": [ProductFeatures.AIRECOGNITION_SETTING]}
544+
)
545+
is_ai_recognition_obstacle_supported: bool = field(
546+
metadata={"product_features": [ProductFeatures.AIRECOGNITION_OBSTACLE]}
547+
)
542548
is_pure_clean_mop_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_PURECLEANMOP]})
543549
is_new_remote_view_supported: bool = field(metadata={"product_features": [ProductFeatures.REMOTE_BACK]})
544550
is_max_plus_mode_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_MAXPLUS]})
@@ -670,6 +676,8 @@ def get_supported_features(self) -> list[str]:
670676
RoborockDockTypeCode.o4_dock,
671677
RoborockDockTypeCode.o5_dock,
672678
RoborockDockTypeCode.o6_dock,
679+
RoborockDockTypeCode.o7_dock,
680+
RoborockDockTypeCode.o7h_dock,
673681
}
674682

675683
_CLEAN_FLUID_AUTO_DELIVERY_DOCK_TYPES = {
@@ -679,6 +687,10 @@ def get_supported_features(self) -> list[str]:
679687
RoborockDockTypeCode.shell_3s_dock,
680688
RoborockDockTypeCode.o6_dock,
681689
RoborockDockTypeCode.couple_dock,
690+
RoborockDockTypeCode.type_27_dock,
691+
RoborockDockTypeCode.o7_dock,
692+
RoborockDockTypeCode.f1s_dock,
693+
RoborockDockTypeCode.o7h_dock,
682694
}
683695

684696
_CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = {
@@ -690,6 +702,7 @@ def get_supported_features(self) -> list[str]:
690702
RoborockDockTypeCode.shell_e_dock,
691703
RoborockDockTypeCode.shell_2e_dock,
692704
RoborockDockTypeCode.shell_3c_dock,
705+
RoborockDockTypeCode.k1c_dock,
693706
}
694707

695708
_AUTO_STERILIZE_DOCK_TYPES = {
@@ -719,6 +732,18 @@ def get_supported_features(self) -> list[str]:
719732
RoborockDockTypeCode.shell_3s_dock,
720733
RoborockDockTypeCode.k1s_dock,
721734
RoborockDockTypeCode.shell_3c_dock,
735+
RoborockDockTypeCode.k1s_pro_dock,
736+
RoborockDockTypeCode.type_27_dock,
737+
RoborockDockTypeCode.k1r_dock,
738+
RoborockDockTypeCode.shell_4rc_dock,
739+
RoborockDockTypeCode.shell_4p_dock,
740+
RoborockDockTypeCode.shell_4s_dock,
741+
RoborockDockTypeCode.k1d_dock,
742+
RoborockDockTypeCode.shell_3p_dock,
743+
RoborockDockTypeCode.f1_dock,
744+
RoborockDockTypeCode.f2r_dock,
745+
RoborockDockTypeCode.f1s_dock,
746+
RoborockDockTypeCode.f1c_dock,
722747
}
723748

724749
_WATER_UPDOWN_DRAIN_DOCK_TYPES = {
@@ -739,6 +764,19 @@ def get_supported_features(self) -> list[str]:
739764
RoborockDockTypeCode.shell_e_dock,
740765
RoborockDockTypeCode.shell_2e_dock,
741766
RoborockDockTypeCode.shell_3c_dock,
767+
RoborockDockTypeCode.k1c_dock,
768+
RoborockDockTypeCode.hera_dock,
769+
RoborockDockTypeCode.k1s_pro_dock,
770+
RoborockDockTypeCode.type_27_dock,
771+
RoborockDockTypeCode.shell_4rc_dock,
772+
RoborockDockTypeCode.shell_4r_dock,
773+
RoborockDockTypeCode.shell_4p_dock,
774+
RoborockDockTypeCode.shell_4s_dock,
775+
RoborockDockTypeCode.o7_dock,
776+
RoborockDockTypeCode.shell_4d_dock,
777+
RoborockDockTypeCode.f1r_dock,
778+
RoborockDockTypeCode.f1s_dock,
779+
RoborockDockTypeCode.o7h_dock,
742780
}
743781

744782
_HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = {
@@ -749,6 +787,56 @@ def get_supported_features(self) -> list[str]:
749787
RoborockDockTypeCode.shell_2e_lite_dock,
750788
}
751789

790+
_HIGH_TMP_WATER_DOCK_TYPES = {
791+
RoborockDockTypeCode.k1r_dock,
792+
RoborockDockTypeCode.shell_4rc_dock,
793+
RoborockDockTypeCode.shell_2e_heat_dock,
794+
RoborockDockTypeCode.f1_dock,
795+
RoborockDockTypeCode.f1r_dock,
796+
RoborockDockTypeCode.f2r_dock,
797+
RoborockDockTypeCode.f1c_dock,
798+
}
799+
800+
_DOUBLE_SERIAL_COMMUNICATION_DOCK_TYPES = {
801+
RoborockDockTypeCode.k1s_pro_dock,
802+
RoborockDockTypeCode.type_27_dock,
803+
RoborockDockTypeCode.k1r_dock,
804+
RoborockDockTypeCode.shell_4r_dock,
805+
RoborockDockTypeCode.shell_4p_dock,
806+
RoborockDockTypeCode.shell_4s_dock,
807+
RoborockDockTypeCode.o7_dock,
808+
RoborockDockTypeCode.k1d_dock,
809+
RoborockDockTypeCode.shell_3p_dock,
810+
RoborockDockTypeCode.f1_dock,
811+
RoborockDockTypeCode.f1r_dock,
812+
RoborockDockTypeCode.f2r_dock,
813+
RoborockDockTypeCode.f1s_dock,
814+
RoborockDockTypeCode.o7h_dock,
815+
RoborockDockTypeCode.f1c_dock,
816+
}
817+
818+
_DOUBLE_SERIAL_COMMUNICATION_AM_DOCK_TYPES = {
819+
RoborockDockTypeCode.shell_4rc_dock,
820+
RoborockDockTypeCode.shell_4d_dock,
821+
}
822+
823+
_NO_WATER_FLOW_CAROUSEL_DOCK_TYPES = {
824+
RoborockDockTypeCode.f1r_dock,
825+
RoborockDockTypeCode.f2r_dock,
826+
RoborockDockTypeCode.f1s_dock,
827+
}
828+
829+
_SIMPLE_WASH_MODE_DOCK_TYPES = {
830+
RoborockDockTypeCode.f1_dock,
831+
RoborockDockTypeCode.f1r_dock,
832+
RoborockDockTypeCode.f2r_dock,
833+
RoborockDockTypeCode.f1c_dock,
834+
}
835+
836+
_STEAM_WASH_DOCK_TYPES = {
837+
RoborockDockTypeCode.o7h_dock,
838+
}
839+
752840

753841
@dataclass(frozen=True)
754842
class RoborockDockFeatures:
@@ -849,6 +937,28 @@ def is_hatch_door_dock_cool_fan_supported(self) -> bool:
849937
def is_special_support_wash_temp(self) -> bool:
850938
return self.dock_type in _SPECIAL_WASH_TEMP_DOCK_TYPES
851939

940+
@property
941+
def is_high_tmp_water_supported(self) -> bool:
942+
return self.dock_type in _HIGH_TMP_WATER_DOCK_TYPES
943+
944+
@property
945+
def is_double_serial_communication_supported(self) -> bool:
946+
return self.dock_type in _DOUBLE_SERIAL_COMMUNICATION_DOCK_TYPES or self._matches_am_variant(
947+
_DOUBLE_SERIAL_COMMUNICATION_AM_DOCK_TYPES
948+
)
949+
950+
@property
951+
def is_no_water_flow_carousel(self) -> bool:
952+
return self.dock_type in _NO_WATER_FLOW_CAROUSEL_DOCK_TYPES
953+
954+
@property
955+
def is_simple_wash_mode_supported(self) -> bool:
956+
return self.dock_type in _SIMPLE_WASH_MODE_DOCK_TYPES
957+
958+
@property
959+
def is_steam_wash_supported(self) -> bool:
960+
return self.dock_type in _STEAM_WASH_DOCK_TYPES
961+
852962

853963
WASH_N_FILL_DOCK_TYPES = [
854964
dock_type for dock_type in RoborockDockTypeCode if RoborockDockFeatures.from_dock_type(dock_type).is_washable

roborock/devices/device_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
246246
channel.rpc_channel,
247247
channel.mqtt_rpc_channel,
248248
channel.map_rpc_channel,
249+
channel.blob_rpc_channel,
249250
channel.add_dps_listener,
250251
web_api,
251252
device_cache=device_cache,

roborock/devices/rpc/v1_channel.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ResponseMessage,
3030
SecurityData,
3131
V1RpcChannel,
32+
create_blob_response_decoder,
3233
create_map_response_decoder,
3334
create_security_data,
3435
decode_data_protocol_message,
@@ -161,6 +162,50 @@ def find_response(response_message: RoborockMessage) -> None:
161162
return result
162163

163164

165+
class BlobRpcChannel(V1RpcChannel):
166+
"""RPC channel that adds the security envelope required for blob requests."""
167+
168+
def __init__(
169+
self,
170+
rpc_channel: V1RpcChannel,
171+
raw_blob_rpc_channel: V1RpcChannel,
172+
security_data: SecurityData,
173+
) -> None:
174+
"""Initialize the blob RPC channel."""
175+
self._rpc_channel = rpc_channel
176+
self._raw_blob_rpc_channel = raw_blob_rpc_channel
177+
self._security_data = security_data
178+
179+
async def send_command(
180+
self,
181+
method: CommandType,
182+
*,
183+
response_type: type[_T] | None = None,
184+
params: ParamsType = None,
185+
) -> _T | Any:
186+
"""Send a blob command after adding the device security parameters."""
187+
public_key = await self._rpc_channel.send_command(RoborockCommand.GET_RANDOM_PKEY)
188+
if not isinstance(public_key, dict) or not isinstance(public_key.get("pub_key"), dict):
189+
raise RoborockException("get_random_pkey response did not contain a public key")
190+
security = self._security_data.to_dict()["security"]
191+
blob_params = {
192+
"security": {
193+
"pub_key": public_key["pub_key"],
194+
"cipher_suite": 0,
195+
},
196+
"endpoint": security["endpoint"],
197+
"nonce": security["nonce"],
198+
"data_filter": params,
199+
}
200+
if response_type is not None:
201+
return await self._raw_blob_rpc_channel.send_command(
202+
method,
203+
response_type=response_type,
204+
params=blob_params,
205+
)
206+
return await self._raw_blob_rpc_channel.send_command(method, params=blob_params)
207+
208+
164209
class V1Channel(Channel):
165210
"""Unified V1 protocol channel with automatic MQTT/local connection handling.
166211
@@ -245,6 +290,13 @@ def map_rpc_channel(self) -> V1RpcChannel:
245290
decoder = create_map_response_decoder(security_data=self._security_data)
246291
return RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
247292

293+
@property
294+
def blob_rpc_channel(self) -> V1RpcChannel:
295+
"""Return the blob RPC channel used for fetching binary content."""
296+
decoder = create_blob_response_decoder()
297+
raw_blob_rpc_channel = RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger)
298+
return BlobRpcChannel(self.rpc_channel, raw_blob_rpc_channel, self._security_data)
299+
248300
def _create_local_rpc_strategy(self) -> RpcStrategy | None:
249301
"""Create the RPC strategy for local transport."""
250302
if self._local_channel is None or not self.is_local_connected:

0 commit comments

Comments
 (0)