Repository navigation
Refactor WaterModes to use display_name instead of StrEnum value aliases #987
Description
Problem
RoborockModeEnum subclasses StrEnum and takes (value: str, code: int, display_name: str | None = None). Because StrEnum deduplicates members strictly by their first argument (value: str) and ignores code, any two members in the same enum that share a value string silently collapse into a single enum member and discard the second member's code.
In WaterModes (roborock/data/v1/v1_clean_modes.py), four water-slide members reuse the same value strings as earlier legacy members:
class WaterModes(RoborockModeEnum):
LOW = ("low", 201)
MEDIUM = ("medium", 202)
HIGH = ("high", 203)
EXTREME = ("extreme", 208)
...
PURE_WATER_FLOW_SMALL = ("low", 225) # Collapses into LOW (code=201)
PURE_WATER_FLOW_MIDDLE = ("medium", 235) # Collapses into MEDIUM (code=202)
PURE_WATER_SUPER_BEGIN = ("high", 245) # Collapses into HIGH (code=203)
PURE_WATER_FLOW_END = ("extreme", 250) # Collapses into EXTREME (code=208)At runtime:
WaterModes.PURE_WATER_FLOW_MIDDLE is WaterModes.MEDIUMisTrueWaterModes.PURE_WATER_FLOW_MIDDLE.codeis202(instead of235)WaterModes.from_code_optional(235)returnsNone(same for225,245,250)
Because the enum members lose their own .code values, v1_clean_modes.py has had to accumulate workarounds (#777, #961):
- Maintaining a separate
WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes]side-table just to preserve the integer codes225,235,245, and250. - In
get_cleaning_mode_parameters(), ignoringwater_box_mode.codeon water-slide devices and doing a reverse lookup overget_water_mode_mapping(features)bywater_box_mode.value(next(code for code, name in ... if name == water_box_mode.value)). - In
get_current_cleaning_mode(), calling_resolve_mode_code(water_mode, WaterModes)and then patchingwater_mode_enuminline withif features.is_water_slide_mode_supported and isinstance(water_mode, int):.
Suggested Fix
1. Use display_name on WaterModes (matching CleanRoutes from #918)
RoborockModeEnum already supports a 3rd display_name parameter specifically for modes that have distinct wire codes but share a canonical user-facing name (e.g. CleanRoutes.DEEP_PLUS_CN = ("deep_plus_cn", 305, "deep_plus")):
class WaterModes(RoborockModeEnum):
...
PURE_WATER_FLOW_START = ("slight", 221)
PURE_WATER_FLOW_SMALL = ("pure_water_flow_small", 225, "low")
PURE_WATER_FLOW_GENTLE = ("gentle", 230)
PURE_WATER_FLOW_MIDDLE = ("pure_water_flow_middle", 235, "medium")
PURE_WATER_FLOW_LARGE = ("moderate", 240)
PURE_WATER_SUPER_BEGIN = ("pure_water_super_begin", 245, "high")
PURE_WATER_FLOW_END = ("pure_water_flow_end", 250, "extreme")With this change:
- Every
WaterModesmember is a distinct enum entry with its own.code(WaterModes.PURE_WATER_FLOW_MIDDLE.code == 235). WaterModes.PURE_WATER_FLOW_MIDDLE.display_name == "medium".WaterModes.from_code_optional(235)returnsWaterModes.PURE_WATER_FLOW_MIDDLE.
2. Simplify v1_clean_modes.py
get_water_mode_mapping(features): Usemode.display_nameuniformly across all devices (matchingStatusTrait.mop_route_mapping):def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]: return {mode.code: mode.display_name for mode in get_water_modes(features)}
get_water_slide_mode_name(code): Returnmode.display_name if mode is not None else None.get_cleaning_mode_parameters(cleaning_mode, features): Remove theif features.is_water_slide_mode_supported: water_code = next(...)workaround and usewater_box_mode.codedirectly.get_current_cleaning_mode(...): Extract_resolve_water_mode(value, features)parallel to_resolve_clean_mode(value, features)instead of checkingisinstance(water_mode, int)inline.
3. Guard against future silent RoborockModeEnum alias collisions
Add a check in RoborockModeEnum.__new__ (or in tests/conformance/test_enum_conformance.py) that fails if two members in a RoborockModeEnum subclass share the same value string with different code integers instead of using display_name.
Proposed Upgrade Plan
Downstream in Home Assistant Core (homeassistant/components/roborock/select.py), mop_mode already uses mode.display_name, but water_box_mode still reads mode.value:
RoborockSelectDescription(
key="water_box_mode",
translation_key="mop_intensity",
api_command=RoborockCommand.SET_WATER_BOX_CUSTOM_MODE,
value_fn=lambda api: api.status.water_mode_name,
entity_category=EntityCategory.CONFIG,
options_lambda=lambda api: (
[mode.value for mode in api.status.water_mode_options] # <-- uses .value instead of .display_name
if api.status.water_mode_options
else None
),
...
),To make this migration seamless and non-breaking:
- Step 1 (Home Assistant Core): Update
water_box_modeinhomeassistant/components/roborock/select.pyfrom[mode.value for mode in api.status.water_mode_options]to[mode.display_name for mode in api.status.water_mode_options]. BecauseRoborockModeEnum.display_namefalls back toself.valuewhen_display_name_isNone, this is a no-op withpython-roborockv7.12.4and can be merged independently (or alongside the dependency bump). - Step 2 (
python-roborock): Land theWaterModesdisplay_namefix, remove the workarounds inv1_clean_modes.py, and add the enum conformance check. - Step 3 (Home Assistant Core): Bump
python-roborockin Home Assistant Core. User-facing option strings ("off","slight","low","gentle","medium","moderate","high","extreme") andwater_mode_mappingremain unchanged.
Activity
solsxia commentedon Oct 17, 2026
Hi! I'm interested in this issue and would like to work on it. Could you assign it to me? Thank you.
rohitpatil9121 commentedon Oct 19, 2026
Hi! I've opened a draft PR for this: #993. It follows the approach in the issue description: unique values with
display_namefor the water slide modes, the workarounds removed, and a conformance check for value aliases.@solsxia sorry, I only saw your comment after I'd started. Happy to coordinate if you were already working on this.