roborock.data.v1.v1_clean_modes

  1from __future__ import annotations
  2
  3import typing
  4from enum import StrEnum
  5from typing import TypeVar
  6
  7from ...exceptions import RoborockUnsupportedFeature
  8from ..code_mappings import RoborockModeEnum
  9
 10if typing.TYPE_CHECKING:
 11    from roborock.device_features import DeviceFeatures
 12
 13
 14class VacuumModes(RoborockModeEnum):
 15    GENTLE = ("gentle", 105)
 16    OFF = ("off", 105)
 17    QUIET = ("quiet", 101)
 18    BALANCED = ("balanced", 102)
 19    TURBO = ("turbo", 103)
 20    MAX = ("max", 104)
 21    MAX_PLUS = ("max_plus", 108)
 22    OFF_RAISE_MAIN_BRUSH = ("off_raise_main_brush", 109)
 23    CUSTOMIZED = ("custom", 106)
 24    SMART_MODE = ("smart_mode", 110)
 25
 26
 27class CleanRoutes(RoborockModeEnum):
 28    STANDARD = ("standard", 300)
 29    DEEP = ("deep", 301)
 30    DEEP_PLUS = ("deep_plus", 303)
 31    FAST = ("fast", 304)
 32    DEEP_PLUS_CN = ("deep_plus_cn", 305, "deep_plus")
 33    SMART_MODE = ("smart_mode", 306)
 34    CUSTOMIZED = ("custom", 302)
 35
 36
 37class VacuumModesOld(RoborockModeEnum):
 38    QUIET = ("quiet", 38)
 39    BALANCED = ("balanced", 60)
 40    TURBO = ("turbo", 75)
 41    MAX = ("max", 100)
 42
 43
 44class WaterModes(RoborockModeEnum):
 45    OFF = ("off", 200)
 46    LOW = ("low", 201)
 47    MILD = ("mild", 201)
 48    MEDIUM = ("medium", 202)
 49    STANDARD = ("standard", 202)
 50    HIGH = ("high", 203)
 51    INTENSE = ("intense", 203)
 52    MIN = ("min", 205)
 53    MAX = ("max", 206)
 54    CUSTOMIZED = ("custom", 204)
 55    CUSTOM = ("custom_water_flow", 207)
 56    EXTREME = ("extreme", 208)
 57    SMART_MODE = ("smart_mode", 209)
 58    PURE_WATER_FLOW_START = ("slight", 221)
 59    PURE_WATER_FLOW_SMALL = ("low", 225)
 60    PURE_WATER_FLOW_MIDDLE = ("medium", 235)
 61    PURE_WATER_FLOW_LARGE = ("moderate", 245)
 62    PURE_WATER_SUPER_BEGIN = ("high", 248)
 63    PURE_WATER_FLOW_END = ("extreme", 250)
 64
 65
 66class WashTowelModes(RoborockModeEnum):
 67    SMART = ("smart", 10)
 68    LIGHT = ("light", 0)
 69    BALANCED = ("balanced", 1)
 70    DEEP = ("deep", 2)
 71    SUPER_DEEP = ("super_deep", 8)
 72
 73
 74class 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    fine-grained selection.
 81    """
 82
 83    VACUUM = "vacuum"
 84    VAC_AND_MOP = "vac_and_mop"
 85    MOP = "mop"
 86    CUSTOM = "custom"
 87    SMART_MODE = "smart_mode"
 88
 89
 90WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
 91    200: WaterModes.OFF,
 92    221: WaterModes.PURE_WATER_FLOW_START,
 93    225: WaterModes.PURE_WATER_FLOW_SMALL,
 94    235: WaterModes.PURE_WATER_FLOW_MIDDLE,
 95    245: WaterModes.PURE_WATER_FLOW_LARGE,
 96    248: WaterModes.PURE_WATER_SUPER_BEGIN,
 97    250: WaterModes.PURE_WATER_FLOW_END,
 98}
 99
100ModeEnumT = TypeVar("ModeEnumT", bound=RoborockModeEnum)
101
102
103def get_wash_towel_modes(features: DeviceFeatures) -> list[WashTowelModes]:
104    """Get the valid wash towel modes for the device"""
105    modes = [WashTowelModes.LIGHT, WashTowelModes.BALANCED, WashTowelModes.DEEP]
106    if features.is_super_deep_wash_supported and not features.is_dirty_replenish_clean_supported:
107        modes.append(WashTowelModes.SUPER_DEEP)
108    elif features.is_dirty_replenish_clean_supported:
109        modes.append(WashTowelModes.SMART)
110    return modes
111
112
113def get_clean_modes(features: DeviceFeatures) -> list[VacuumModes]:
114    """Get the valid clean modes for the device - also known as 'fan power' or 'suction mode'"""
115    modes = [VacuumModes.QUIET, VacuumModes.BALANCED, VacuumModes.TURBO, VacuumModes.MAX]
116    if features.is_max_plus_mode_supported or features.is_none_pure_clean_mop_with_max_plus:
117        # If the vacuum has max plus mode supported
118        modes.append(VacuumModes.MAX_PLUS)
119    if features.is_pure_clean_mop_supported:
120        # If the vacuum is capable of 'pure mop clean' aka no vacuum
121        modes.append(VacuumModes.OFF)
122    else:
123        # If not, we can add gentle
124        modes.append(VacuumModes.GENTLE)
125    if features.is_smart_clean_mode_set_supported:
126        modes.append(VacuumModes.SMART_MODE)
127    if features.is_customized_clean_supported:
128        modes.append(VacuumModes.CUSTOMIZED)
129    return modes
130
131
132def get_clean_routes(features: DeviceFeatures, region: str) -> list[CleanRoutes]:
133    """The routes that the vacuum will take while mopping"""
134    if not features.is_clean_route_setting_supported:
135        return []
136    if features.is_none_pure_clean_mop_with_max_plus:
137        return [CleanRoutes.FAST, CleanRoutes.STANDARD]
138    supported = [CleanRoutes.STANDARD]
139    if not features.is_clean_efficiency_supported:
140        supported.append(CleanRoutes.DEEP)
141    if features.is_careful_slow_mop_supported:
142        if (
143            not features.is_corner_clean_mode_supported
144            and features.is_clean_route_deep_slow_plus_supported
145            and region == "cn"
146        ):
147            supported.append(CleanRoutes.DEEP_PLUS_CN)
148        else:
149            supported.append(CleanRoutes.DEEP_PLUS)
150
151    if features.is_clean_route_fast_mode_supported:
152        supported.append(CleanRoutes.FAST)
153    if features.is_smart_clean_mode_set_supported:
154        supported.append(CleanRoutes.SMART_MODE)
155    if features.is_customized_clean_supported:
156        supported.append(CleanRoutes.CUSTOMIZED)
157
158    return supported
159
160
161def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
162    """Get the valid water modes for the device - also known as 'water flow' or 'water level'"""
163    # Water slide mode supports a separate set of water flow codes.
164    if features.is_water_slide_mode_supported:
165        return list(WATER_SLIDE_MODE_MAPPING.values())
166
167    supported_modes = [WaterModes.OFF]
168    if features.is_mop_shake_module_supported:
169        # For mops that have the vibrating mop pad, they do mild standard intense
170        supported_modes.extend([WaterModes.MILD, WaterModes.STANDARD, WaterModes.INTENSE])
171    else:
172        supported_modes.extend([WaterModes.LOW, WaterModes.MEDIUM, WaterModes.HIGH])
173    if features.is_custom_water_box_distance_supported:
174        # This is for devices that allow you to set a custom water flow from 0-100
175        supported_modes.append(WaterModes.CUSTOM)
176    if features.is_mop_shake_module_supported and features.is_mop_shake_water_max_supported:
177        supported_modes.append(WaterModes.EXTREME)
178    if features.is_smart_clean_mode_set_supported:
179        supported_modes.append(WaterModes.SMART_MODE)
180    if features.is_customized_clean_supported:
181        supported_modes.append(WaterModes.CUSTOMIZED)
182
183    return supported_modes
184
185
186def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
187    """Get water mode mapping by supported feature set.
188
189    WaterModes contains aliases for multiple codes that share the same value
190    string (e.g. low can be 201 or 225). For water slide mode devices we need
191    explicit code mapping to preserve those slide-specific codes.
192    """
193    if features.is_water_slide_mode_supported:
194        return {code: mode.value for code, mode in WATER_SLIDE_MODE_MAPPING.items()}
195    return {mode.code: mode.value for mode in get_water_modes(features)}
196
197
198def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningMode]:
199    """Return the supported high-level cleaning modes for the device.
200
201    These options are the preferred user-facing choices because they bundle the
202    correct fan, water, and mop-route settings together for the device. Callers
203    should generally present these instead of mixing lower-level mode enums
204    unless they explicitly need fine-grained control.
205    """
206    if not features.is_support_water_mode:
207        return []
208
209    supported_water_modes = get_water_modes(features)
210    options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
211    if features.is_pure_clean_mop_supported:
212        options.append(CleaningMode.MOP)
213    if features.is_customized_clean_supported and WaterModes.CUSTOMIZED in supported_water_modes:
214        options.append(CleaningMode.CUSTOM)
215    if features.is_smart_clean_mode_set_supported and WaterModes.SMART_MODE in supported_water_modes:
216        options.append(CleaningMode.SMART_MODE)
217    return options
218
219
220def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
221    """Return the vacuum mode used by the app for mop-only cleaning."""
222    if not features.is_pure_clean_mop_supported:
223        raise RoborockUnsupportedFeature("Mop-only cleaning is not supported")
224    # Main-brush lift is a device capability, not an alternate fan-power
225    # command. The app still sends CleanModeZero (105) for mop-only cleaning.
226    return VacuumModes.OFF
227
228
229def _get_default_mopping_water_mode(features: DeviceFeatures) -> WaterModes:
230    """Pick a sensible default water mode when mopping for the device."""
231    # Water-slide devices use a disjoint set of water codes; pick a mid-flow
232    # slide code instead of the standard 202, which they don't accept.
233    if features.is_water_slide_mode_supported:
234        return WaterModes.PURE_WATER_FLOW_MIDDLE
235    return WaterModes.STANDARD
236
237
238def _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:
248        return (
249            get_mop_only_vacuum_mode(features),
250            _get_default_mopping_water_mode(features),
251            CleanRoutes.STANDARD,
252        )
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)
257    raise RoborockUnsupportedFeature(f"Cleaning mode {mode.value!r} is not supported")
258
259
260def 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
264    try:
265        return CleaningMode(cleaning_mode)
266    except ValueError as err:
267        raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err
268
269
270def 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}
277    if features.is_clean_route_setting_supported:
278        params["mop_mode"] = mop_mode.code
279    return [params]
280
281
282def _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
291def _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
302def get_current_cleaning_mode(
303    clean_mode: int | VacuumModes | None,
304    water_mode: int | WaterModes | None,
305    mop_mode: int | CleanRoutes | None,
306    features: DeviceFeatures,
307) -> CleaningMode | None:
308    """Classify the current high-level cleaning mode from individual mode codes."""
309    if not features.is_support_water_mode:
310        return 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:
315        return None
316
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:
322        try:
323            if clean_mode_enum == get_mop_only_vacuum_mode(features):
324                return CleaningMode.MOP
325        except RoborockUnsupportedFeature:
326            pass
327    if water_mode_enum == WaterModes.OFF:
328        return CleaningMode.VACUUM
329    return CleaningMode.VAC_AND_MOP
330
331
332def is_mode_customized(
333    clean_mode: VacuumModes | None,
334    water_mode: WaterModes | None,
335    mop_mode: CleanRoutes | None,
336) -> bool:
337    """Check if any of the cleaning modes are set to a custom value."""
338    return (
339        clean_mode == VacuumModes.CUSTOMIZED
340        or water_mode == WaterModes.CUSTOMIZED
341        or mop_mode == CleanRoutes.CUSTOMIZED
342    )
343
344
345def is_smart_mode_set(
346    water_mode: WaterModes | None,
347    clean_mode: VacuumModes | None,
348    mop_mode: CleanRoutes | None,
349) -> bool:
350    """Check if the smart mode is set for the given water mode and clean mode"""
351    return (
352        water_mode == WaterModes.SMART_MODE
353        or clean_mode == VacuumModes.SMART_MODE
354        or mop_mode == CleanRoutes.SMART_MODE
355    )
class VacuumModes(roborock.data.code_mappings.RoborockModeEnum):
15class VacuumModes(RoborockModeEnum):
16    GENTLE = ("gentle", 105)
17    OFF = ("off", 105)
18    QUIET = ("quiet", 101)
19    BALANCED = ("balanced", 102)
20    TURBO = ("turbo", 103)
21    MAX = ("max", 104)
22    MAX_PLUS = ("max_plus", 108)
23    OFF_RAISE_MAIN_BRUSH = ("off_raise_main_brush", 109)
24    CUSTOMIZED = ("custom", 106)
25    SMART_MODE = ("smart_mode", 110)

A custom StrEnum that also stores an integer code for each member.

GENTLE = <VacuumModes.GENTLE: 'gentle'>
OFF = <VacuumModes.OFF: 'off'>
QUIET = <VacuumModes.QUIET: 'quiet'>
BALANCED = <VacuumModes.BALANCED: 'balanced'>
TURBO = <VacuumModes.TURBO: 'turbo'>
MAX = <VacuumModes.MAX: 'max'>
MAX_PLUS = <VacuumModes.MAX_PLUS: 'max_plus'>
OFF_RAISE_MAIN_BRUSH = <VacuumModes.OFF_RAISE_MAIN_BRUSH: 'off_raise_main_brush'>
CUSTOMIZED = <VacuumModes.CUSTOMIZED: 'custom'>
SMART_MODE = <VacuumModes.SMART_MODE: 'smart_mode'>
class CleanRoutes(roborock.data.code_mappings.RoborockModeEnum):
28class CleanRoutes(RoborockModeEnum):
29    STANDARD = ("standard", 300)
30    DEEP = ("deep", 301)
31    DEEP_PLUS = ("deep_plus", 303)
32    FAST = ("fast", 304)
33    DEEP_PLUS_CN = ("deep_plus_cn", 305, "deep_plus")
34    SMART_MODE = ("smart_mode", 306)
35    CUSTOMIZED = ("custom", 302)

A custom StrEnum that also stores an integer code for each member.

STANDARD = <CleanRoutes.STANDARD: 'standard'>
DEEP = <CleanRoutes.DEEP: 'deep'>
DEEP_PLUS = <CleanRoutes.DEEP_PLUS: 'deep_plus'>
FAST = <CleanRoutes.FAST: 'fast'>
DEEP_PLUS_CN = <CleanRoutes.DEEP_PLUS_CN: 'deep_plus_cn'>
SMART_MODE = <CleanRoutes.SMART_MODE: 'smart_mode'>
CUSTOMIZED = <CleanRoutes.CUSTOMIZED: 'custom'>
class VacuumModesOld(roborock.data.code_mappings.RoborockModeEnum):
38class VacuumModesOld(RoborockModeEnum):
39    QUIET = ("quiet", 38)
40    BALANCED = ("balanced", 60)
41    TURBO = ("turbo", 75)
42    MAX = ("max", 100)

A custom StrEnum that also stores an integer code for each member.

QUIET = <VacuumModesOld.QUIET: 'quiet'>
BALANCED = <VacuumModesOld.BALANCED: 'balanced'>
TURBO = <VacuumModesOld.TURBO: 'turbo'>
MAX = <VacuumModesOld.MAX: 'max'>
class WaterModes(roborock.data.code_mappings.RoborockModeEnum):
45class WaterModes(RoborockModeEnum):
46    OFF = ("off", 200)
47    LOW = ("low", 201)
48    MILD = ("mild", 201)
49    MEDIUM = ("medium", 202)
50    STANDARD = ("standard", 202)
51    HIGH = ("high", 203)
52    INTENSE = ("intense", 203)
53    MIN = ("min", 205)
54    MAX = ("max", 206)
55    CUSTOMIZED = ("custom", 204)
56    CUSTOM = ("custom_water_flow", 207)
57    EXTREME = ("extreme", 208)
58    SMART_MODE = ("smart_mode", 209)
59    PURE_WATER_FLOW_START = ("slight", 221)
60    PURE_WATER_FLOW_SMALL = ("low", 225)
61    PURE_WATER_FLOW_MIDDLE = ("medium", 235)
62    PURE_WATER_FLOW_LARGE = ("moderate", 245)
63    PURE_WATER_SUPER_BEGIN = ("high", 248)
64    PURE_WATER_FLOW_END = ("extreme", 250)

A custom StrEnum that also stores an integer code for each member.

OFF = <WaterModes.OFF: 'off'>
LOW = <WaterModes.LOW: 'low'>
MILD = <WaterModes.MILD: 'mild'>
MEDIUM = <WaterModes.MEDIUM: 'medium'>
STANDARD = <WaterModes.STANDARD: 'standard'>
HIGH = <WaterModes.HIGH: 'high'>
INTENSE = <WaterModes.INTENSE: 'intense'>
MIN = <WaterModes.MIN: 'min'>
MAX = <WaterModes.MAX: 'max'>
CUSTOMIZED = <WaterModes.CUSTOMIZED: 'custom'>
CUSTOM = <WaterModes.CUSTOM: 'custom_water_flow'>
EXTREME = <WaterModes.EXTREME: 'extreme'>
SMART_MODE = <WaterModes.SMART_MODE: 'smart_mode'>
PURE_WATER_FLOW_START = <WaterModes.PURE_WATER_FLOW_START: 'slight'>
PURE_WATER_FLOW_SMALL = <WaterModes.LOW: 'low'>
PURE_WATER_FLOW_MIDDLE = <WaterModes.MEDIUM: 'medium'>
PURE_WATER_FLOW_LARGE = <WaterModes.PURE_WATER_FLOW_LARGE: 'moderate'>
PURE_WATER_SUPER_BEGIN = <WaterModes.HIGH: 'high'>
PURE_WATER_FLOW_END = <WaterModes.EXTREME: 'extreme'>
class WashTowelModes(roborock.data.code_mappings.RoborockModeEnum):
67class WashTowelModes(RoborockModeEnum):
68    SMART = ("smart", 10)
69    LIGHT = ("light", 0)
70    BALANCED = ("balanced", 1)
71    DEEP = ("deep", 2)
72    SUPER_DEEP = ("super_deep", 8)

A custom StrEnum that also stores an integer code for each member.

SMART = <WashTowelModes.SMART: 'smart'>
LIGHT = <WashTowelModes.LIGHT: 'light'>
BALANCED = <WashTowelModes.BALANCED: 'balanced'>
DEEP = <WashTowelModes.DEEP: 'deep'>
SUPER_DEEP = <WashTowelModes.SUPER_DEEP: 'super_deep'>
class CleaningMode(enum.StrEnum):
75class CleaningMode(StrEnum):
76    """High-level cleaning intent derived from the lower-level motor settings.
77
78    Prefer this abstraction when you want to present or switch between the
79    user-facing cleaning behaviors exposed by the app. The lower-level
80    `VacuumModes`, `WaterModes`, and `CleanRoutes` enums are still useful for
81    fine-grained selection.
82    """
83
84    VACUUM = "vacuum"
85    VAC_AND_MOP = "vac_and_mop"
86    MOP = "mop"
87    CUSTOM = "custom"
88    SMART_MODE = "smart_mode"

High-level cleaning intent derived from the lower-level motor settings.

Prefer this abstraction when you want to present or switch between the user-facing cleaning behaviors exposed by the app. The lower-level VacuumModes, WaterModes, and CleanRoutes enums are still useful for fine-grained selection.

VACUUM = <CleaningMode.VACUUM: 'vacuum'>
VAC_AND_MOP = <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>
MOP = <CleaningMode.MOP: 'mop'>
CUSTOM = <CleaningMode.CUSTOM: 'custom'>
SMART_MODE = <CleaningMode.SMART_MODE: 'smart_mode'>
WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {200: <WaterModes.OFF: 'off'>, 221: <WaterModes.PURE_WATER_FLOW_START: 'slight'>, 225: <WaterModes.LOW: 'low'>, 235: <WaterModes.MEDIUM: 'medium'>, 245: <WaterModes.PURE_WATER_FLOW_LARGE: 'moderate'>, 248: <WaterModes.HIGH: 'high'>, 250: <WaterModes.EXTREME: 'extreme'>}
def get_wash_towel_modes( features: roborock.device_features.DeviceFeatures) -> list[WashTowelModes]:
104def get_wash_towel_modes(features: DeviceFeatures) -> list[WashTowelModes]:
105    """Get the valid wash towel modes for the device"""
106    modes = [WashTowelModes.LIGHT, WashTowelModes.BALANCED, WashTowelModes.DEEP]
107    if features.is_super_deep_wash_supported and not features.is_dirty_replenish_clean_supported:
108        modes.append(WashTowelModes.SUPER_DEEP)
109    elif features.is_dirty_replenish_clean_supported:
110        modes.append(WashTowelModes.SMART)
111    return modes

Get the valid wash towel modes for the device

def get_clean_modes( features: roborock.device_features.DeviceFeatures) -> list[VacuumModes]:
114def get_clean_modes(features: DeviceFeatures) -> list[VacuumModes]:
115    """Get the valid clean modes for the device - also known as 'fan power' or 'suction mode'"""
116    modes = [VacuumModes.QUIET, VacuumModes.BALANCED, VacuumModes.TURBO, VacuumModes.MAX]
117    if features.is_max_plus_mode_supported or features.is_none_pure_clean_mop_with_max_plus:
118        # If the vacuum has max plus mode supported
119        modes.append(VacuumModes.MAX_PLUS)
120    if features.is_pure_clean_mop_supported:
121        # If the vacuum is capable of 'pure mop clean' aka no vacuum
122        modes.append(VacuumModes.OFF)
123    else:
124        # If not, we can add gentle
125        modes.append(VacuumModes.GENTLE)
126    if features.is_smart_clean_mode_set_supported:
127        modes.append(VacuumModes.SMART_MODE)
128    if features.is_customized_clean_supported:
129        modes.append(VacuumModes.CUSTOMIZED)
130    return modes

Get the valid clean modes for the device - also known as 'fan power' or 'suction mode'

def get_clean_routes( features: roborock.device_features.DeviceFeatures, region: str) -> list[CleanRoutes]:
133def get_clean_routes(features: DeviceFeatures, region: str) -> list[CleanRoutes]:
134    """The routes that the vacuum will take while mopping"""
135    if not features.is_clean_route_setting_supported:
136        return []
137    if features.is_none_pure_clean_mop_with_max_plus:
138        return [CleanRoutes.FAST, CleanRoutes.STANDARD]
139    supported = [CleanRoutes.STANDARD]
140    if not features.is_clean_efficiency_supported:
141        supported.append(CleanRoutes.DEEP)
142    if features.is_careful_slow_mop_supported:
143        if (
144            not features.is_corner_clean_mode_supported
145            and features.is_clean_route_deep_slow_plus_supported
146            and region == "cn"
147        ):
148            supported.append(CleanRoutes.DEEP_PLUS_CN)
149        else:
150            supported.append(CleanRoutes.DEEP_PLUS)
151
152    if features.is_clean_route_fast_mode_supported:
153        supported.append(CleanRoutes.FAST)
154    if features.is_smart_clean_mode_set_supported:
155        supported.append(CleanRoutes.SMART_MODE)
156    if features.is_customized_clean_supported:
157        supported.append(CleanRoutes.CUSTOMIZED)
158
159    return supported

The routes that the vacuum will take while mopping

def get_water_modes( features: roborock.device_features.DeviceFeatures) -> list[WaterModes]:
162def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
163    """Get the valid water modes for the device - also known as 'water flow' or 'water level'"""
164    # Water slide mode supports a separate set of water flow codes.
165    if features.is_water_slide_mode_supported:
166        return list(WATER_SLIDE_MODE_MAPPING.values())
167
168    supported_modes = [WaterModes.OFF]
169    if features.is_mop_shake_module_supported:
170        # For mops that have the vibrating mop pad, they do mild standard intense
171        supported_modes.extend([WaterModes.MILD, WaterModes.STANDARD, WaterModes.INTENSE])
172    else:
173        supported_modes.extend([WaterModes.LOW, WaterModes.MEDIUM, WaterModes.HIGH])
174    if features.is_custom_water_box_distance_supported:
175        # This is for devices that allow you to set a custom water flow from 0-100
176        supported_modes.append(WaterModes.CUSTOM)
177    if features.is_mop_shake_module_supported and features.is_mop_shake_water_max_supported:
178        supported_modes.append(WaterModes.EXTREME)
179    if features.is_smart_clean_mode_set_supported:
180        supported_modes.append(WaterModes.SMART_MODE)
181    if features.is_customized_clean_supported:
182        supported_modes.append(WaterModes.CUSTOMIZED)
183
184    return supported_modes

Get the valid water modes for the device - also known as 'water flow' or 'water level'

def get_water_mode_mapping(features: roborock.device_features.DeviceFeatures) -> dict[int, str]:
187def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
188    """Get water mode mapping by supported feature set.
189
190    WaterModes contains aliases for multiple codes that share the same value
191    string (e.g. low can be 201 or 225). For water slide mode devices we need
192    explicit code mapping to preserve those slide-specific codes.
193    """
194    if features.is_water_slide_mode_supported:
195        return {code: mode.value for code, mode in WATER_SLIDE_MODE_MAPPING.items()}
196    return {mode.code: mode.value for mode in get_water_modes(features)}

Get water mode mapping by supported feature set.

WaterModes contains aliases for multiple codes that share the same value string (e.g. low can be 201 or 225). For water slide mode devices we need explicit code mapping to preserve those slide-specific codes.

def get_cleaning_mode_options( features: roborock.device_features.DeviceFeatures) -> list[CleaningMode]:
199def 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    """
207    if not features.is_support_water_mode:
208        return []
209
210    supported_water_modes = get_water_modes(features)
211    options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
212    if features.is_pure_clean_mop_supported:
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)
218    return options

Return the supported high-level cleaning modes for the device.

These options are the preferred user-facing choices because they bundle the correct fan, water, and mop-route settings together for the device. Callers should generally present these instead of mixing lower-level mode enums unless they explicitly need fine-grained control.

def get_mop_only_vacuum_mode( features: roborock.device_features.DeviceFeatures) -> VacuumModes:
221def get_mop_only_vacuum_mode(features: DeviceFeatures) -> VacuumModes:
222    """Return the vacuum mode used by the app for mop-only cleaning."""
223    if not features.is_pure_clean_mop_supported:
224        raise RoborockUnsupportedFeature("Mop-only cleaning is not supported")
225    # Main-brush lift is a device capability, not an alternate fan-power
226    # command. The app still sends CleanModeZero (105) for mop-only cleaning.
227    return VacuumModes.OFF

Return the vacuum mode used by the app for mop-only cleaning.

def resolve_cleaning_mode( cleaning_mode: str | CleaningMode) -> CleaningMode:
261def resolve_cleaning_mode(cleaning_mode: str | CleaningMode) -> CleaningMode:
262    """Resolve a string or enum into a CleaningMode value."""
263    if isinstance(cleaning_mode, CleaningMode):
264        return cleaning_mode
265    try:
266        return CleaningMode(cleaning_mode)
267    except ValueError as err:
268        raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode!r} is not supported") from err

Resolve a string or enum into a CleaningMode value.

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

Get the RPC payload for switching the high-level cleaning mode.

def get_current_cleaning_mode( clean_mode: int | VacuumModes | None, water_mode: int | WaterModes | None, mop_mode: int | CleanRoutes | None, features: roborock.device_features.DeviceFeatures) -> CleaningMode | None:
303def get_current_cleaning_mode(
304    clean_mode: int | VacuumModes | None,
305    water_mode: int | WaterModes | None,
306    mop_mode: int | CleanRoutes | None,
307    features: DeviceFeatures,
308) -> CleaningMode | None:
309    """Classify the current high-level cleaning mode from individual mode codes."""
310    if not features.is_support_water_mode:
311        return None
312    clean_mode_enum = _resolve_clean_mode(clean_mode, features)
313    water_mode_enum = _resolve_mode_code(water_mode, WaterModes)
314    mop_mode_enum = _resolve_mode_code(mop_mode, CleanRoutes)
315    if clean_mode_enum is None or water_mode_enum is None:
316        return None
317
318    if is_smart_mode_set(water_mode_enum, clean_mode_enum, mop_mode_enum):
319        return CleaningMode.SMART_MODE
320    if is_mode_customized(clean_mode_enum, water_mode_enum, mop_mode_enum):
321        return CleaningMode.CUSTOM
322    if water_mode_enum != WaterModes.OFF:
323        try:
324            if clean_mode_enum == get_mop_only_vacuum_mode(features):
325                return CleaningMode.MOP
326        except RoborockUnsupportedFeature:
327            pass
328    if water_mode_enum == WaterModes.OFF:
329        return CleaningMode.VACUUM
330    return CleaningMode.VAC_AND_MOP

Classify the current high-level cleaning mode from individual mode codes.

def is_mode_customized( clean_mode: VacuumModes | None, water_mode: WaterModes | None, mop_mode: CleanRoutes | None) -> bool:
333def is_mode_customized(
334    clean_mode: VacuumModes | None,
335    water_mode: WaterModes | None,
336    mop_mode: CleanRoutes | None,
337) -> bool:
338    """Check if any of the cleaning modes are set to a custom value."""
339    return (
340        clean_mode == VacuumModes.CUSTOMIZED
341        or water_mode == WaterModes.CUSTOMIZED
342        or mop_mode == CleanRoutes.CUSTOMIZED
343    )

Check if any of the cleaning modes are set to a custom value.

def is_smart_mode_set( water_mode: WaterModes | None, clean_mode: VacuumModes | None, mop_mode: CleanRoutes | None) -> bool:
346def is_smart_mode_set(
347    water_mode: WaterModes | None,
348    clean_mode: VacuumModes | None,
349    mop_mode: CleanRoutes | None,
350) -> bool:
351    """Check if the smart mode is set for the given water mode and clean mode"""
352    return (
353        water_mode == WaterModes.SMART_MODE
354        or clean_mode == VacuumModes.SMART_MODE
355        or mop_mode == CleanRoutes.SMART_MODE
356    )

Check if the smart mode is set for the given water mode and clean mode