11from __future__ import annotations
22
33import typing
4+ from enum import StrEnum
5+ from typing import TypeVar
46
7+ from ...exceptions import RoborockUnsupportedFeature
58from ..code_mappings import RoborockModeEnum
69
710if typing .TYPE_CHECKING :
@@ -68,6 +71,22 @@ class WashTowelModes(RoborockModeEnum):
6871 SUPER_DEEP = ("super_deep" , 8 )
6972
7073
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+ 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+
7190WATER_SLIDE_MODE_MAPPING : dict [int , WaterModes ] = {
7291 200 : WaterModes .OFF ,
7392 221 : WaterModes .PURE_WATER_FLOW_START ,
@@ -78,6 +97,8 @@ class WashTowelModes(RoborockModeEnum):
7897 250 : WaterModes .PURE_WATER_FLOW_END ,
7998}
8099
100+ ModeEnumT = TypeVar ("ModeEnumT" , bound = RoborockModeEnum )
101+
81102
82103def get_wash_towel_modes (features : DeviceFeatures ) -> list [WashTowelModes ]:
83104 """Get the valid wash towel modes for the device"""
@@ -174,7 +195,151 @@ def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
174195 return {mode .code : mode .value for mode in get_water_modes (features )}
175196
176197
177- def is_mode_customized (clean_mode : VacuumModes , water_mode : WaterModes , mop_mode : CleanRoutes ) -> bool :
198+ def 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+
220+ def get_mop_only_vacuum_mode (features : DeviceFeatures ) -> VacuumModes :
221+ """Determine the vacuum mode to use when you just want to mop.
222+
223+ There are three cases that must be handled:
224+ 1. The device does not support only mopping.
225+ 2. The device supports raising the vacuum brush while mopping
226+ 3. All other cases.
227+ """
228+ if not features .is_pure_clean_mop_supported :
229+ raise RoborockUnsupportedFeature ("Mop-only cleaning is not supported" )
230+ if features .is_support_main_brush_up_down_supported :
231+ return VacuumModes .OFF_RAISE_MAIN_BRUSH
232+ return VacuumModes .OFF
233+
234+
235+ def _get_default_mopping_water_mode (features : DeviceFeatures ) -> WaterModes :
236+ """Pick a sensible default water mode when mopping for the device."""
237+ # Water-slide devices use a disjoint set of water codes; pick a mid-flow
238+ # slide code instead of the standard 202, which they don't accept.
239+ if features .is_water_slide_mode_supported :
240+ return WaterModes .PURE_WATER_FLOW_MIDDLE
241+ return WaterModes .STANDARD
242+
243+
244+ def _get_clean_motor_mode_params (
245+ mode : CleaningMode ,
246+ features : DeviceFeatures ,
247+ ) -> tuple [VacuumModes , WaterModes , CleanRoutes ]:
248+ """Return (fan_power, water_box_mode, mop_mode) enums for the high-level mode."""
249+ if mode == CleaningMode .VACUUM :
250+ return (VacuumModes .BALANCED , WaterModes .OFF , CleanRoutes .STANDARD )
251+ if mode == CleaningMode .VAC_AND_MOP :
252+ return (VacuumModes .BALANCED , _get_default_mopping_water_mode (features ), CleanRoutes .STANDARD )
253+ if mode == CleaningMode .MOP :
254+ return (
255+ get_mop_only_vacuum_mode (features ),
256+ _get_default_mopping_water_mode (features ),
257+ CleanRoutes .STANDARD ,
258+ )
259+ if mode == CleaningMode .CUSTOM :
260+ return (VacuumModes .CUSTOMIZED , WaterModes .CUSTOMIZED , CleanRoutes .CUSTOMIZED )
261+ if mode == CleaningMode .SMART_MODE :
262+ return (VacuumModes .SMART_MODE , WaterModes .SMART_MODE , CleanRoutes .SMART_MODE )
263+ raise RoborockUnsupportedFeature (f"Cleaning mode { mode .value !r} is not supported" )
264+
265+
266+ def resolve_cleaning_mode (cleaning_mode : str | CleaningMode ) -> CleaningMode :
267+ """Resolve a string or enum into a CleaningMode value."""
268+ if isinstance (cleaning_mode , CleaningMode ):
269+ return cleaning_mode
270+ try :
271+ return CleaningMode (cleaning_mode )
272+ except ValueError as err :
273+ raise RoborockUnsupportedFeature (f"Cleaning mode { cleaning_mode !r} is not supported" ) from err
274+
275+
276+ def get_cleaning_mode_parameters (cleaning_mode : CleaningMode , features : DeviceFeatures ) -> list [dict [str , int ]]:
277+ """Get the RPC payload for switching the high-level cleaning mode."""
278+ if cleaning_mode not in get_cleaning_mode_options (features ):
279+ raise RoborockUnsupportedFeature (f"Cleaning mode { cleaning_mode .value !r} is not supported" )
280+
281+ fan_power , water_box_mode , mop_mode = _get_clean_motor_mode_params (cleaning_mode , features )
282+ params : dict [str , int ] = {"fan_power" : fan_power .code , "water_box_mode" : water_box_mode .code }
283+ if features .is_clean_route_setting_supported :
284+ params ["mop_mode" ] = mop_mode .code
285+ return [params ]
286+
287+
288+ def _resolve_mode_code (value : int | ModeEnumT | None , mode_cls : type [ModeEnumT ]) -> ModeEnumT | None :
289+ """Resolve a raw code or enum into a RoborockModeEnum."""
290+ if value is None :
291+ return None
292+ if isinstance (value , mode_cls ):
293+ return value
294+ return mode_cls .from_code_optional (int (value ))
295+
296+
297+ def _resolve_clean_mode (value : int | VacuumModes | None , features : DeviceFeatures ) -> VacuumModes | None :
298+ """Resolve a vacuum mode code, accounting for feature-specific code aliases."""
299+ if value is None or isinstance (value , VacuumModes ):
300+ return value
301+ if value == VacuumModes .OFF .code :
302+ if features .is_pure_clean_mop_supported :
303+ return get_mop_only_vacuum_mode (features )
304+ return VacuumModes .GENTLE
305+ return VacuumModes .from_code_optional (value )
306+
307+
308+ def get_current_cleaning_mode (
309+ clean_mode : int | VacuumModes | None ,
310+ water_mode : int | WaterModes | None ,
311+ mop_mode : int | CleanRoutes | None ,
312+ features : DeviceFeatures ,
313+ ) -> CleaningMode | None :
314+ """Classify the current high-level cleaning mode from individual mode codes."""
315+ if not features .is_support_water_mode :
316+ return None
317+ clean_mode_enum = _resolve_clean_mode (clean_mode , features )
318+ water_mode_enum = _resolve_mode_code (water_mode , WaterModes )
319+ mop_mode_enum = _resolve_mode_code (mop_mode , CleanRoutes )
320+ if clean_mode_enum is None or water_mode_enum is None :
321+ return None
322+
323+ if is_smart_mode_set (water_mode_enum , clean_mode_enum , mop_mode_enum ):
324+ return CleaningMode .SMART_MODE
325+ if is_mode_customized (clean_mode_enum , water_mode_enum , mop_mode_enum ):
326+ return CleaningMode .CUSTOM
327+ if water_mode_enum != WaterModes .OFF :
328+ try :
329+ if clean_mode_enum == get_mop_only_vacuum_mode (features ):
330+ return CleaningMode .MOP
331+ except RoborockUnsupportedFeature :
332+ pass
333+ if water_mode_enum == WaterModes .OFF :
334+ return CleaningMode .VACUUM
335+ return CleaningMode .VAC_AND_MOP
336+
337+
338+ def is_mode_customized (
339+ clean_mode : VacuumModes | None ,
340+ water_mode : WaterModes | None ,
341+ mop_mode : CleanRoutes | None ,
342+ ) -> bool :
178343 """Check if any of the cleaning modes are set to a custom value."""
179344 return (
180345 clean_mode == VacuumModes .CUSTOMIZED
@@ -183,7 +348,11 @@ def is_mode_customized(clean_mode: VacuumModes, water_mode: WaterModes, mop_mode
183348 )
184349
185350
186- def is_smart_mode_set (water_mode : WaterModes , clean_mode : VacuumModes , mop_mode : CleanRoutes ) -> bool :
351+ def is_smart_mode_set (
352+ water_mode : WaterModes | None ,
353+ clean_mode : VacuumModes | None ,
354+ mop_mode : CleanRoutes | None ,
355+ ) -> bool :
187356 """Check if the smart mode is set for the given water mode and clean mode"""
188357 return (
189358 water_mode == WaterModes .SMART_MODE
0 commit comments