Repository navigation
WaterModes pure-water members collapse into StrEnum aliases, sending water code 202 to water-slide devices (-10005 Param error) #931
Description
Summary
Four WaterModes members share a value string with an earlier member. StrEnum de-duplicates by value, so they silently become aliases and carry the earlier member's code.
The visible consequence: _get_default_mopping_water_mode() returns PURE_WATER_FLOW_MIDDLE specifically to avoid code 202 (per its own comment, water-slide devices "don't accept" it), but the alias resolves to MEDIUM, so set_cleaning_mode() sends 202 to exactly those devices and they reject the command.
Version
python-roborock==7.1.1 (current release, and the version Home Assistant pins). Reproduced on a clean install, CPython 3.13.
Reproduction
from roborock.data.v1.v1_clean_modes import WaterModes, WATER_SLIDE_MODE_MAPPING
print(len(list(WaterModes))) # 15, though 19 names are declared
print(WaterModes.PURE_WATER_FLOW_MIDDLE is WaterModes.MEDIUM) # True
print(WaterModes.PURE_WATER_FLOW_MIDDLE.code) # 202, expected 235
print(WaterModes.from_code_optional(235)) # None
for code, mode in WATER_SLIDE_MODE_MAPPING.items():
if code != mode.code:
print(code, "->", mode.name, mode.code)
# 225 -> LOW 201
# 235 -> MEDIUM 202
# 248 -> HIGH 203
# 250 -> EXTREME 208The four collapsing pairs are ("low", 225) into LOW, ("medium", 235) into MEDIUM, ("high", 248) into HIGH, and ("extreme", 250) into EXTREME.
Impact
On a device with is_water_slide_mode_supported, get_cleaning_mode_parameters() emits {'fan_power': 105, 'water_box_mode': 202, 'mop_mode': 300} for CleaningMode.MOP (and the same 202 for VAC_AND_MOP). Observed on a Saros 20 Sonic (roborock.vacuum.a288) through the Home Assistant integration:
File ".../roborock/devices/traits/v1/status.py", line 131, in set_cleaning_mode
await self.rpc_channel.send_command(
roborock.exceptions.RoborockException: {'code': -10005, 'message': 'Param error'}
Sending the same command with 235 instead of 202 succeeds on that device.
Secondary: from_code_optional() returns None for 225, 235, 248 and 250, so those device codes cannot be decoded through the enum.
The display path is unaffected, because get_water_mode_mapping() keys off the dict rather than the member. This only surfaces on writes.
Suggested fix
Unique values plus display_name, matching the existing DEEP_PLUS_CN = ("deep_plus_cn", 305, "deep_plus") precedent:
- PURE_WATER_FLOW_SMALL = ("low", 225)
- PURE_WATER_FLOW_MIDDLE = ("medium", 235)
- PURE_WATER_SUPER_BEGIN = ("high", 248)
- PURE_WATER_FLOW_END = ("extreme", 250)
+ PURE_WATER_FLOW_SMALL = ("pure_water_flow_small", 225, "low")
+ PURE_WATER_FLOW_MIDDLE = ("pure_water_flow_middle", 235, "medium")
+ PURE_WATER_SUPER_BEGIN = ("pure_water_super_begin", 248, "high")
+ PURE_WATER_FLOW_END = ("pure_water_flow_end", 250, "extreme")and in get_water_mode_mapping(), read mode.display_name instead of mode.value in both branches.
Verified with that patch applied to 7.1.1: 19 members, all four codes correct, from_code_optional() resolves every slide code, WATER_SLIDE_MODE_MAPPING round-trips, and get_water_mode_mapping() still returns {200: 'off', 221: 'slight', 225: 'low', 235: 'medium', 245: 'moderate', 248: 'high', 250: 'extreme'}, so the user-facing strings do not change.
One follow-up outside this repo: Home Assistant's select.py builds the water options from mode.value and would need mode.display_name, as its mop-route select already does.
Happy to open a PR.
Activity
liborplisek commentedon Aug 28, 2026
Cross-reference: #930 reports the user-visible side of this on a Saros 10R (
roborock.vacuum.a144), independently and with iOS app packet captures showing the correct values. Samewater_box_mode: 202and the same-10005 Param error, so this is not specific to one model.That report also describes the cleaning-mode entity going
unknownafter a manually corrected command. That is the same root cause:get_current_cleaning_mode()resolves the reported water code throughWaterModes.from_code_optional(), which returnsNonefor 225, 235, 248 and 250, so the classification returnsNone.Verified against a clean install of 7.1.1, CPython 3.13:
So the patch above fixes both the rejected write and the unresolvable state.
xiaoyaner0201 commentedon Sep 16, 2026
Follow-up implementation: #961
Confirmed the same combined-mode read/write problem on a G30S Ultra with Home Assistant 2026.9.1 and python-roborock 7.2.3. Thank you for the diagnosis above; I am adding evidence here rather than filing a duplicate.
The device reported
fan_power=102, water_box_mode=235, mop_mode=303. The individual settings correctly read balanced/medium/deep_plus, butcurrent_cleaning_modewasNone(HAunknown). This stopped a scheduled automation at its mode-readback gate beforevacuum.start. The generated combined-mode command also contained the incorrect water code202.Using the existing feature-aware water mapping for both combined-mode command encoding and status classification restored the mode to
vac_and_mop. The real automation then passed its setting gate and started cleaning; independent device readback confirmed the expected motor settings, no error, and increasing activity counters.I also found that the existing water-slide tests expected
WaterModes.PURE_WATER_FLOW_MIDDLE.code, so they passed with the same erroneous202as the implementation. Replacing those expectations with actual wire integers and adding public-trait regressions produces 19 assertion failures before the fix and 79 passing status tests afterward.The linked compatibility-focused PR preserves enum/UI names and existing legacy-code fallback. It does not claim that the standalone enum aliases themselves are fixed; your proposed unique-value/display-name migration is a separate, broader change. It also does not change the slider ranges discussed in #801.
koltuknil commentedon Sep 27, 2026
Adding evidence from a Saros 20 Sonic (
roborock.vacuum.a279), in case it helps with #961. Two things differ from the reports above: this model accepts the wrong code instead of rejecting it, and we saw a possible side effect.Environment: Home Assistant 2026.9.3, which pins python-roborock 7.4.2. The robot's water select offers
off, slight, low, medium, moderate, high, extreme, so it uses the water-slide mapping.Still present in 7.12.0
The reproduction above gives the same result against the library's own source at both 7.4.2 and 7.12.0. We loaded
v1_clean_modes.pyandcode_mappings.pydirectly and used a stand-in object with the water-slide feature flags:This model accepts water code 202
Unlike the a288 above and the Saros 10R in #930, this robot does not reject
set_clean_motor_modewithwater_box_mode: 202.medium, which it can only decode from 235, so the robot appears to translate 202 itself.So on some water-slide models the wrong write is silent rather than an error. (The code 202 comes from reading the source and the reproduction above; we did not capture the traffic.)
Read path, observed through Home Assistant
The robot was docked, and we made one change at a time through the cleaning-mode select, reading the states and the app after each.
max/extreme/deep_plusunknownvac_and_mopbalanced/medium/standardunknownvacuumbalanced/off/standardvacuummopoff/medium/standardunknownThis matches
from_code_optional()returningNonefor 235 and 250: the select reads correctly only when the water code resolves.Possible side effect of selecting mop (not proven)
mopthrough Home Assistant, the app's scrub-intensity setting showed no level selected. This was in mop mode, and afterwards also in vac & mop mode, until the level was set again in the app.vac_and_mopearlier, with the same water code 202, left scrub intensity unchanged.We haven't tested #961 yet, but we're happy to try it on this model if that helps.