Skip to content

Commit 35e7354

Browse files
committed
feat: add better cleaning mode support
1 parent 53cee21 commit 35e7354

4 files changed

Lines changed: 366 additions & 7 deletions

File tree

roborock/data/v1/v1_clean_modes.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

33
import typing
4+
from enum import StrEnum
45

6+
from ...exceptions import RoborockUnsupportedFeature
57
from ..code_mappings import RoborockModeEnum
68

79
if typing.TYPE_CHECKING:
@@ -68,6 +70,14 @@ class WashTowelModes(RoborockModeEnum):
6870
SUPER_DEEP = ("super_deep", 8)
6971

7072

73+
class CleaningModes(StrEnum):
74+
VACUUM = "vacuum"
75+
VAC_AND_MOP = "vac_and_mop"
76+
MOP = "mop"
77+
CUSTOM = "custom"
78+
SMART_MODE = "smart_mode"
79+
80+
7181
WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
7282
200: WaterModes.OFF,
7383
221: WaterModes.PURE_WATER_FLOW_START,
@@ -174,7 +184,90 @@ def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
174184
return {mode.code: mode.value for mode in get_water_modes(features)}
175185

176186

177-
def is_mode_customized(clean_mode: VacuumModes, water_mode: WaterModes, mop_mode: CleanRoutes) -> bool:
187+
def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningModes]:
188+
"""Get the supported high-level cleaning modes for the device."""
189+
if not features.is_support_water_mode:
190+
return []
191+
192+
options = [CleaningModes.VACUUM, CleaningModes.VAC_AND_MOP]
193+
if features.is_pure_clean_mop_supported:
194+
options.append(CleaningModes.MOP)
195+
if features.is_customized_clean_supported:
196+
options.append(CleaningModes.CUSTOM)
197+
if features.is_smart_clean_mode_set_supported:
198+
options.append(CleaningModes.SMART_MODE)
199+
return options
200+
201+
202+
def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
203+
if not features.is_pure_clean_mop_supported:
204+
raise RoborockUnsupportedFeature("Mop-only cleaning is not supported")
205+
if features.is_support_main_brush_up_down_supported:
206+
return VacuumModes.OFF_RAISE_MAIN_BRUSH
207+
return VacuumModes.OFF
208+
209+
210+
_CLEAN_MOTOR_MODE_PARAMS: dict[CleaningModes, tuple[int, int, int]] = {
211+
CleaningModes.VACUUM: (VacuumModes.BALANCED.code, WaterModes.OFF.code, CleanRoutes.STANDARD.code),
212+
CleaningModes.VAC_AND_MOP: (VacuumModes.BALANCED.code, WaterModes.STANDARD.code, CleanRoutes.STANDARD.code),
213+
CleaningModes.CUSTOM: (VacuumModes.CUSTOMIZED.code, WaterModes.CUSTOMIZED.code, CleanRoutes.CUSTOMIZED.code),
214+
CleaningModes.SMART_MODE: (VacuumModes.SMART_MODE.code, WaterModes.SMART_MODE.code, CleanRoutes.SMART_MODE.code),
215+
}
216+
217+
218+
def get_cleaning_mode_parameters(cleaning_mode: str | CleaningModes, features: DeviceFeatures) -> list[dict[str, int]]:
219+
"""Get the RPC payload for switching the high-level cleaning mode."""
220+
try:
221+
mode = CleaningModes(cleaning_mode)
222+
except ValueError as err:
223+
raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err
224+
if mode not in get_cleaning_mode_options(features):
225+
raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
226+
227+
if mode == CleaningModes.MOP:
228+
fan_power = get_mop_only_vacuum_mode(features).code
229+
water_box_mode = WaterModes.STANDARD.code
230+
mop_mode = CleanRoutes.STANDARD.code
231+
else:
232+
fan_power, water_box_mode, mop_mode = _CLEAN_MOTOR_MODE_PARAMS[mode]
233+
234+
params: dict[str, int] = {"fan_power": fan_power, "water_box_mode": water_box_mode}
235+
if features.is_clean_route_setting_supported:
236+
params["mop_mode"] = mop_mode
237+
return [params]
238+
239+
240+
def get_current_cleaning_mode(
241+
clean_mode: int | None,
242+
water_mode: int | None,
243+
mop_mode: int | None,
244+
features: DeviceFeatures,
245+
) -> CleaningModes | None:
246+
"""Classify the current high-level cleaning mode from individual mode codes."""
247+
if not features.is_support_water_mode:
248+
return None
249+
if clean_mode is None or water_mode is None:
250+
return None
251+
252+
if is_smart_mode_set(water_mode, clean_mode, mop_mode):
253+
return CleaningModes.SMART_MODE
254+
if is_mode_customized(clean_mode, water_mode, mop_mode):
255+
return CleaningModes.CUSTOM
256+
try:
257+
if clean_mode == get_mop_only_vacuum_mode(features).code:
258+
return CleaningModes.MOP
259+
except RoborockUnsupportedFeature:
260+
pass
261+
if water_mode == WaterModes.OFF.code:
262+
return CleaningModes.VACUUM
263+
return CleaningModes.VAC_AND_MOP
264+
265+
266+
def is_mode_customized(
267+
clean_mode: int | VacuumModes | None,
268+
water_mode: int | WaterModes | None,
269+
mop_mode: int | CleanRoutes | None,
270+
) -> bool:
178271
"""Check if any of the cleaning modes are set to a custom value."""
179272
return (
180273
clean_mode == VacuumModes.CUSTOMIZED
@@ -183,7 +276,11 @@ def is_mode_customized(clean_mode: VacuumModes, water_mode: WaterModes, mop_mode
183276
)
184277

185278

186-
def is_smart_mode_set(water_mode: WaterModes, clean_mode: VacuumModes, mop_mode: CleanRoutes) -> bool:
279+
def is_smart_mode_set(
280+
water_mode: int | WaterModes | None,
281+
clean_mode: int | VacuumModes | None,
282+
mop_mode: int | CleanRoutes | None,
283+
) -> bool:
187284
"""Check if the smart mode is set for the given water mode and clean mode"""
188285
return (
189286
water_mode == WaterModes.SMART_MODE

roborock/devices/traits/v1/status.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from functools import cached_property
22

33
from roborock import (
4+
CleaningModes,
45
CleanRoutes,
56
StatusV2,
67
VacuumModes,
78
WaterModes,
89
get_clean_modes,
910
get_clean_routes,
11+
get_cleaning_mode_options,
12+
get_cleaning_mode_parameters,
13+
get_current_cleaning_mode,
1014
get_water_mode_mapping,
1115
get_water_modes,
1216
)
@@ -34,9 +38,10 @@ class StatusTrait(StatusV2, common.V1TraitMixin):
3438
- Water Mode
3539
- Mop Route
3640
37-
You should call the _options() version of the attribute to know which are supported for your device
38-
(i.e. fan_speed_options())
39-
Then you can call the _mapping to convert an int value to the actual Enum. (i.e. fan_speed_mapping())
41+
You should call the _options() version of the attribute to know which are
42+
supported for your device (i.e. fan_speed_options())
43+
Then you can call the _mapping to convert an int value to the actual Enum.
44+
(i.e. fan_speed_mapping())
4045
You can call the _name property to get the str value of the enum. (i.e. fan_speed_name)
4146
4247
"""
@@ -74,6 +79,10 @@ def mop_route_options(self) -> list[CleanRoutes]:
7479
def mop_route_mapping(self) -> dict[int, str]:
7580
return {route.code: route.value for route in self.mop_route_options}
7681

82+
@cached_property
83+
def cleaning_mode_options(self) -> list[CleaningModes]:
84+
return get_cleaning_mode_options(self._device_features_trait)
85+
7786
@property
7887
def fan_speed_name(self) -> str | None:
7988
if self.fan_power is None:
@@ -91,3 +100,29 @@ def mop_route_name(self) -> str | None:
91100
if self.mop_mode is None:
92101
return None
93102
return self.mop_route_mapping.get(self.mop_mode)
103+
104+
@property
105+
def current_cleaning_mode(self) -> CleaningModes | None:
106+
return get_current_cleaning_mode(
107+
clean_mode=self.fan_power,
108+
water_mode=self.water_box_mode,
109+
mop_mode=self.mop_mode,
110+
features=self._device_features_trait,
111+
)
112+
113+
@property
114+
def cleaning_mode_name(self) -> str | None:
115+
if (cleaning_mode := self.current_cleaning_mode) is None:
116+
return None
117+
return cleaning_mode.value
118+
119+
def get_cleaning_mode_parameters(self, cleaning_mode: str | CleaningModes) -> list[dict[str, int]]:
120+
"""Get the RPC payload for the selected high-level cleaning mode."""
121+
return get_cleaning_mode_parameters(cleaning_mode, self._device_features_trait)
122+
123+
async def set_cleaning_mode(self, cleaning_mode: str | CleaningModes) -> None:
124+
"""Set the high-level cleaning mode."""
125+
await self.rpc_channel.send_command(
126+
RoborockCommand.SET_CLEAN_MOTOR_MODE,
127+
params=self.get_cleaning_mode_parameters(cleaning_mode),
128+
)

tests/devices/__snapshots__/test_v1_device.ambr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@
870870
})
871871
# ---
872872
# name: test_device_trait_command_parsing[status]
873-
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'quiet', 102: 'balanced', 103: 'turbo', 104: 'max', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'standard', 301: 'deep', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'off', 201: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
873+
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, cleaning_mode_name='custom', cleaning_mode_options=[<CleaningModes.VACUUM: 'vacuum'>, <CleaningModes.VAC_AND_MOP: 'vac_and_mop'>, <CleaningModes.MOP: 'mop'>, <CleaningModes.CUSTOM: 'custom'>], clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=<CleaningModes.CUSTOM: 'custom'>, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'quiet', 102: 'balanced', 103: 'turbo', 104: 'max', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'standard', 301: 'deep', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'off', 201: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
874874
# ---
875875
# name: test_device_trait_command_parsing[status].1
876876
dict({

0 commit comments

Comments
 (0)