Skip to content

Commit ed4e3c8

Browse files
committed
chore: address comments
1 parent 68797de commit ed4e3c8

4 files changed

Lines changed: 181 additions & 96 deletions

File tree

roborock/data/v1/v1_clean_modes.py

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

33
import typing
44
from enum import StrEnum
5+
from typing import TypeVar
56

67
from ...exceptions import RoborockUnsupportedFeature
78
from ..code_mappings import RoborockModeEnum
@@ -70,7 +71,16 @@ class WashTowelModes(RoborockModeEnum):
7071
SUPER_DEEP = ("super_deep", 8)
7172

7273

73-
class CleaningModes(StrEnum):
74+
class CleaningMode(StrEnum):
75+
"""High-level cleaning intent derived from the lower-level motor settings.
76+
77+
Prefer this abstraction when you want to present or switch between the
78+
user-facing cleaning behaviors exposed by the app. The lower-level
79+
`VacuumModes`, `WaterModes`, and `CleanRoutes` enums are still useful for
80+
advanced tuning, but most integrations should treat them as implementation
81+
details of a single high-level cleaning mode.
82+
"""
83+
7484
VACUUM = "vacuum"
7585
VAC_AND_MOP = "vac_and_mop"
7686
MOP = "mop"
@@ -88,6 +98,8 @@ class CleaningModes(StrEnum):
8898
250: WaterModes.PURE_WATER_FLOW_END,
8999
}
90100

101+
ModeEnumT = TypeVar("ModeEnumT", bound=RoborockModeEnum)
102+
91103

92104
def get_wash_towel_modes(features: DeviceFeatures) -> list[WashTowelModes]:
93105
"""Get the valid wash towel modes for the device"""
@@ -184,18 +196,25 @@ def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
184196
return {mode.code: mode.value for mode in get_water_modes(features)}
185197

186198

187-
def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningModes]:
188-
"""Get the supported high-level cleaning modes for the device."""
199+
def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningMode]:
200+
"""Return the supported high-level cleaning modes for the device.
201+
202+
These options are the preferred user-facing choices because they bundle the
203+
correct fan, water, and mop-route settings together for the device. Callers
204+
should generally present these instead of mixing lower-level mode enums
205+
unless they explicitly need fine-grained control.
206+
"""
189207
if not features.is_support_water_mode:
190208
return []
191209

192-
options = [CleaningModes.VACUUM, CleaningModes.VAC_AND_MOP]
210+
supported_water_modes = get_water_modes(features)
211+
options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
193212
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)
213+
options.append(CleaningMode.MOP)
214+
if features.is_customized_clean_supported and WaterModes.CUSTOMIZED in supported_water_modes:
215+
options.append(CleaningMode.CUSTOM)
216+
if features.is_smart_clean_mode_set_supported and WaterModes.SMART_MODE in supported_water_modes:
217+
options.append(CleaningMode.SMART_MODE)
199218
return options
200219

201220

@@ -207,75 +226,107 @@ def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
207226
return VacuumModes.OFF
208227

209228

210-
def _get_default_mopping_water_code(features: DeviceFeatures) -> int:
211-
"""Pick a sensible default water code when mopping for the device."""
229+
def _get_default_mopping_water_mode(features: DeviceFeatures) -> WaterModes:
230+
"""Pick a sensible default water mode when mopping for the device."""
212231
# Water-slide devices use a disjoint set of water codes; pick a mid-flow
213232
# slide code instead of the standard 202, which they don't accept.
214233
if features.is_water_slide_mode_supported:
215-
return WaterModes.PURE_WATER_FLOW_MIDDLE.code
216-
return WaterModes.STANDARD.code
234+
return WaterModes.PURE_WATER_FLOW_MIDDLE
235+
return WaterModes.STANDARD
217236

218237

219-
def _get_clean_motor_mode_params(mode: CleaningModes, features: DeviceFeatures) -> tuple[int, int, int]:
220-
"""Return (fan_power, water_box_mode, mop_mode) codes for the high-level mode."""
221-
if mode == CleaningModes.VACUUM:
222-
return (VacuumModes.BALANCED.code, WaterModes.OFF.code, CleanRoutes.STANDARD.code)
223-
if mode == CleaningModes.VAC_AND_MOP:
224-
return (VacuumModes.BALANCED.code, _get_default_mopping_water_code(features), CleanRoutes.STANDARD.code)
225-
if mode == CleaningModes.MOP:
238+
def _get_clean_motor_mode_params(
239+
mode: CleaningMode,
240+
features: DeviceFeatures,
241+
) -> tuple[VacuumModes, WaterModes, CleanRoutes]:
242+
"""Return (fan_power, water_box_mode, mop_mode) enums for the high-level mode."""
243+
if mode == CleaningMode.VACUUM:
244+
return (VacuumModes.BALANCED, WaterModes.OFF, CleanRoutes.STANDARD)
245+
if mode == CleaningMode.VAC_AND_MOP:
246+
return (VacuumModes.BALANCED, _get_default_mopping_water_mode(features), CleanRoutes.STANDARD)
247+
if mode == CleaningMode.MOP:
226248
return (
227-
get_mop_only_vacuum_mode(features).code,
228-
_get_default_mopping_water_code(features),
229-
CleanRoutes.STANDARD.code,
249+
get_mop_only_vacuum_mode(features),
250+
_get_default_mopping_water_mode(features),
251+
CleanRoutes.STANDARD,
230252
)
231-
if mode == CleaningModes.CUSTOM:
232-
return (VacuumModes.CUSTOMIZED.code, WaterModes.CUSTOMIZED.code, CleanRoutes.CUSTOMIZED.code)
233-
if mode == CleaningModes.SMART_MODE:
234-
return (VacuumModes.SMART_MODE.code, WaterModes.SMART_MODE.code, CleanRoutes.SMART_MODE.code)
253+
if mode == CleaningMode.CUSTOM:
254+
return (VacuumModes.CUSTOMIZED, WaterModes.CUSTOMIZED, CleanRoutes.CUSTOMIZED)
255+
if mode == CleaningMode.SMART_MODE:
256+
return (VacuumModes.SMART_MODE, WaterModes.SMART_MODE, CleanRoutes.SMART_MODE)
235257
raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
236258

237259

238-
def get_cleaning_mode_parameters(cleaning_mode: str | CleaningModes, features: DeviceFeatures) -> list[dict[str, int]]:
239-
"""Get the RPC payload for switching the high-level cleaning mode."""
260+
def resolve_cleaning_mode(cleaning_mode: str | CleaningMode) -> CleaningMode:
261+
"""Resolve a string or enum into a CleaningMode value."""
262+
if isinstance(cleaning_mode, CleaningMode):
263+
return cleaning_mode
240264
try:
241-
mode = CleaningModes(cleaning_mode)
265+
return CleaningMode(cleaning_mode)
242266
except ValueError as err:
243267
raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err
244-
if mode not in get_cleaning_mode_options(features):
245-
raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
246268

247-
fan_power, water_box_mode, mop_mode = _get_clean_motor_mode_params(mode, features)
248-
params: dict[str, int] = {"fan_power": fan_power, "water_box_mode": water_box_mode}
269+
270+
def get_cleaning_mode_parameters(cleaning_mode: CleaningMode, features: DeviceFeatures) -> list[dict[str, int]]:
271+
"""Get the RPC payload for switching the high-level cleaning mode."""
272+
if cleaning_mode not in get_cleaning_mode_options(features):
273+
raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode.value!r} is not supported")
274+
275+
fan_power, water_box_mode, mop_mode = _get_clean_motor_mode_params(cleaning_mode, features)
276+
params: dict[str, int] = {"fan_power": fan_power.code, "water_box_mode": water_box_mode.code}
249277
if features.is_clean_route_setting_supported:
250-
params["mop_mode"] = mop_mode
278+
params["mop_mode"] = mop_mode.code
251279
return [params]
252280

253281

282+
def _resolve_mode_code(value: int | ModeEnumT | None, mode_cls: type[ModeEnumT]) -> ModeEnumT | None:
283+
"""Resolve a raw code or enum into a RoborockModeEnum."""
284+
if value is None:
285+
return None
286+
if isinstance(value, mode_cls):
287+
return value
288+
return mode_cls.from_code_optional(int(value))
289+
290+
291+
def _resolve_clean_mode(value: int | VacuumModes | None, features: DeviceFeatures) -> VacuumModes | None:
292+
"""Resolve a vacuum mode code, accounting for feature-specific code aliases."""
293+
if value is None or isinstance(value, VacuumModes):
294+
return value
295+
if value == VacuumModes.OFF.code:
296+
if features.is_pure_clean_mop_supported:
297+
return get_mop_only_vacuum_mode(features)
298+
return VacuumModes.GENTLE
299+
return VacuumModes.from_code_optional(value)
300+
301+
254302
def get_current_cleaning_mode(
255-
clean_mode: int | None,
256-
water_mode: int | None,
257-
mop_mode: int | None,
303+
clean_mode: int | VacuumModes | None,
304+
water_mode: int | WaterModes | None,
305+
mop_mode: int | CleanRoutes | None,
258306
features: DeviceFeatures,
259-
) -> CleaningModes | None:
307+
) -> CleaningMode | None:
260308
"""Classify the current high-level cleaning mode from individual mode codes."""
261309
if not features.is_support_water_mode:
262310
return None
263-
if clean_mode is None or water_mode is None:
311+
clean_mode_enum = _resolve_clean_mode(clean_mode, features)
312+
water_mode_enum = _resolve_mode_code(water_mode, WaterModes)
313+
mop_mode_enum = _resolve_mode_code(mop_mode, CleanRoutes)
314+
if clean_mode_enum is None or water_mode_enum is None:
264315
return None
265316

266-
if is_smart_mode_set(water_mode, clean_mode, mop_mode):
267-
return CleaningModes.SMART_MODE
268-
if is_mode_customized(clean_mode, water_mode, mop_mode):
269-
return CleaningModes.CUSTOM
270-
if water_mode != WaterModes.OFF.code:
317+
if is_smart_mode_set(water_mode_enum, clean_mode_enum, mop_mode_enum):
318+
return CleaningMode.SMART_MODE
319+
if is_mode_customized(clean_mode_enum, water_mode_enum, mop_mode_enum):
320+
return CleaningMode.CUSTOM
321+
if water_mode_enum != WaterModes.OFF:
271322
try:
272-
if clean_mode == get_mop_only_vacuum_mode(features).code:
273-
return CleaningModes.MOP
323+
if clean_mode_enum == get_mop_only_vacuum_mode(features):
324+
return CleaningMode.MOP
274325
except RoborockUnsupportedFeature:
275326
pass
276-
if water_mode == WaterModes.OFF.code:
277-
return CleaningModes.VACUUM
278-
return CleaningModes.VAC_AND_MOP
327+
if water_mode_enum == WaterModes.OFF:
328+
return CleaningMode.VACUUM
329+
return CleaningMode.VAC_AND_MOP
279330

280331

281332
def is_mode_customized(

roborock/devices/traits/v1/status.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from functools import cached_property
22

33
from roborock import (
4-
CleaningModes,
4+
CleaningMode,
55
CleanRoutes,
66
StatusV2,
77
VacuumModes,
@@ -13,6 +13,7 @@
1313
get_current_cleaning_mode,
1414
get_water_mode_mapping,
1515
get_water_modes,
16+
resolve_cleaning_mode,
1617
)
1718
from roborock.roborock_typing import RoborockCommand
1819

@@ -80,7 +81,7 @@ def mop_route_mapping(self) -> dict[int, str]:
8081
return {route.code: route.value for route in self.mop_route_options}
8182

8283
@cached_property
83-
def cleaning_mode_options(self) -> list[CleaningModes]:
84+
def cleaning_mode_options(self) -> list[CleaningMode]:
8485
return get_cleaning_mode_options(self._device_features_trait)
8586

8687
@property
@@ -102,7 +103,7 @@ def mop_route_name(self) -> str | None:
102103
return self.mop_route_mapping.get(self.mop_mode)
103104

104105
@property
105-
def current_cleaning_mode(self) -> CleaningModes | None:
106+
def current_cleaning_mode(self) -> CleaningMode | None:
106107
return get_current_cleaning_mode(
107108
clean_mode=self.fan_power,
108109
water_mode=self.water_box_mode,
@@ -111,18 +112,14 @@ def current_cleaning_mode(self) -> CleaningModes | None:
111112
)
112113

113114
@property
114-
def cleaning_mode_name(self) -> str | None:
115+
def current_cleaning_mode_name(self) -> str | None:
115116
if (cleaning_mode := self.current_cleaning_mode) is None:
116117
return None
117118
return cleaning_mode.value
118119

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."""
120+
async def set_cleaning_mode(self, cleaning_mode: str | CleaningMode) -> None:
121+
"""Set the preferred high-level cleaning mode for the device."""
125122
await self.rpc_channel.send_command(
126123
RoborockCommand.SET_CLEAN_MOTOR_MODE,
127-
params=self.get_cleaning_mode_parameters(cleaning_mode),
124+
params=get_cleaning_mode_parameters(resolve_cleaning_mode(cleaning_mode), self._device_features_trait),
128125
)

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, 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)
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_options=[<CleaningMode.VACUUM: 'vacuum'>, <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>, <CleaningMode.MOP: 'mop'>, <CleaningMode.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=<CleaningMode.CUSTOM: 'custom'>, current_cleaning_mode_name='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)