Skip to content

fix: preserve water-slide codes in cleaning-mode commands and status - #961

Open
xiaoyaner0201 wants to merge 3 commits into
Python-roborock:mainfrom
xiaoyaner0201:fix/water-slide-cleaning-mode
Open

xiaoyaner0201 wants to merge 3 commits into
Python-roborock:mainfrom
xiaoyaner0201:fix/water-slide-cleaning-mode

Conversation

@xiaoyaner0201

@xiaoyaner0201 xiaoyaner0201 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #931 and #930. Thanks to @liborplisek for identifying the enum-alias root cause, @BlackRockCity for the command/status captures, and @frenck for the app range table in #975.

Bring the combined cleaning-mode paths into line with the feature-aware water-mode handling on water-slide devices:

  • Commands: encode water-slide cleaning-mode commands using the device mapping's wire code, so vac_and_mop and mop emit 235 instead of the aliased member's legacy 202.
  • Status, exact codes: resolve reported slide water codes on the device's slide path when classifying the cleaning mode, keeping the existing legacy-code fallback and direct enum-input behavior.
  • Status, in-between slider positions (requested in review): classify any raw slider position 221–250 through the same app range table that water_mode_name already uses since fix: map water slide positions to the app's flow ranges #975. A position set from the app between two named steps (e.g. 223, 248) now yields vac_and_mop / mop instead of None. The raw water_box_mode value is preserved, and the resolver is gated on is_water_slide_mode_supported.
  • Tests: correct existing tests that compared against PURE_WATER_FLOW_MIDDLE.code on both sides (they were passing with the same incorrect 202); expected wire values now use explicit protocol integers. Add public StatusTrait read/write regressions covering every slider position, vacuum/mop selection, optional mop-route fields, legacy codes, enum inputs, capability gating and unknown/incomplete status.

How the range resolution works

get_water_slide_mode_name() and cleaning-mode classification now share one pure helper, _resolve_water_slide_mode(code), built on the existing _WATER_SLIDE_RANGES. There is no second copy of the range boundaries, and get_water_slide_mode_name() returns exactly what it returned before for every input (checked for every integer in range(-10, 400)).

On water-slide devices, an integer water code is classified like this:

  • 200 → off → vacuum
  • 221–250 → the app range for that position → vac_and_mop, or mop with the mop-only fan setting
  • anything else keeps the previous legacy fallback: 201/202/203 → vac_and_mop, 204 → custom, 209 → smart_mode, and 220/251/unknown → None

Devices without is_water_slide_mode_supported are unchanged: an in-between code such as 223 still classifies as None there. Commands still send the exact representative code for the selected mode (235 for vac_and_mop/mop, 200 for vacuum). The range resolver is read-only.

Compatibility and scope

Public enum values, option lists, WATER_SLIDE_MODE_MAPPING, _WATER_SLIDE_RANGES and user-facing option strings are unchanged, so consumers using mode.value need no migration. This fixes the combined-mode command and classification paths, not the standalone enum-alias representation: WaterModes.PURE_WATER_FLOW_MIDDLE.code and generic WaterModes.from_code_optional(235) are still affected. A broader unique-value/display-name migration remains separate, so this PR references rather than closes #931.

Out of scope: water-slide custom/smart_mode option exposure (#977), other protocol families, Home Assistant automation policies, and login fixes.

Overlap with open PR #983: both PRs edit get_water_slide_mode_name(). #983 adds a SMART_WATER early return, and this PR moves that function's body into _resolve_water_slide_mode(). Whichever lands second needs a one-line textual resolution: return WaterModes.SMART_WATER from the helper instead of .value. Happy to rebase onto #983 if it merges first.

Reproduction and hardware evidence

On a G30S Ultra with Home Assistant 2026.9.1 and python-roborock 7.2.3, a real status payload contained fan_power=102, water_box_mode=235, mop_mode=303. Individual settings decoded as balanced/medium/deep_plus, but the combined mode was None (HA unknown). An automation waiting for confirmed vac_and_mop stopped before sending its start command.

After applying the same feature-mapping read/write fix locally, the combined mode reported vac_and_mop, the outgoing combined-mode payload used 235, and the automation passed its setting check and started real cleaning. Direct device readback showed cleaning active with no error and the expected motor settings; the cleaning counters increased. The in-between slider path is covered by unit tests on current main and was not separately re-run on hardware.

Validation

Head merged with main at 6a0aab0543d6812c3eafa92571605444ab74cdb3 (ordinary merge, no rebase or force-push).

  • Regression RED, using this PR's tests against the production code before the range change: 69 assertion failures / 190 tests, all None == <CleaningMode...> for the 23 positions between the seven representative codes, across three fan settings, with no collection errors. Reverting only the classification line reproduces the same 69 failures.
  • GREEN: uv run pytest tests/devices/traits/v1/test_status.py -q — 190 passed.
  • Full suite uv run pytest -q: 1582 passed, 53 xfailed, 11 deselected on Python 3.11 and on Python 3.14. On the same main commit without this PR: 1462 passed, 53 xfailed.
  • uv run pre-commit run --all-files: passed (Ruff, Ruff format, Mypy, Codespell).
  • uv build: source distribution and wheel built.

No device/account credentials, raw diagnostic dumps, or deployment scripts are included.

中文说明

概要

关联 #931 和 #930。感谢 @liborplisek 找出枚举别名的根因,感谢 @BlackRockCity 提供命令和状态抓包,也感谢 @frenck 在 #975 里整理的 App 水量区间表。

让带水量滑块的设备在"组合清扫模式"上的读写,和已有的按设备能力处理水量模式的逻辑保持一致:

  • 下发命令: 滑块设备的清扫模式命令改用设备映射里的实际协议码,vac_and_mop(扫拖)和 mop(纯拖)发 235,不再发别名成员遗留的 202。
  • 状态识别(标准档位): 识别清扫模式时,滑块设备上报的水量码走滑块专用路径解析,同时保留原有的旧码兜底和直接传枚举的行为。
  • 状态识别(滑块中间位置,维护者在 review 中提出): 滑块原始位置 221–250 统一按 water_mode_name 自 fix: map water slide positions to the app's flow ranges #975 起已经在用的同一张 App 区间表来识别。用户在 App 里把滑块停在两个命名档位之间(例如 223、248)时,清扫模式现在识别为 vac_and_mop / mop,不再是 None。设备上报的原始 water_box_mode 值保持不变,区间解析只在 is_water_slide_mode_supported 为真时启用。
  • 测试: 修正了原有测试两边都拿 PURE_WATER_FLOW_MIDDLE.code 比较的问题(两边都是同一个错误的 202,所以一直能过),期望的协议值改成明确写出的整数。新增经公开 StatusTrait 读写的回归测试,覆盖全部滑块位置、扫/拖选择、可选的拖地路线字段、旧码、枚举输入、能力开关,以及未知或不完整的状态。

区间解析怎么工作

get_water_slide_mode_name() 和清扫模式识别现在共用一个纯函数 _resolve_water_slide_mode(code),它基于现有的 _WATER_SLIDE_RANGES,没有再复制一份区间边界。get_water_slide_mode_name() 对任何输入的返回值都和改动前完全一致(已对 range(-10, 400) 内每个整数逐一核对)。

滑块设备上报整数水量码时,识别规则是:

  • 200 → 关水 → vacuum(纯扫)
  • 221–250 → 该位置所在的 App 区间 → vac_and_mop;风机为纯拖档时为 mop
  • 其他值沿用原来的旧码兜底:201/202/203 → vac_and_mop,204 → custom,209 → smart_mode,220/251/未知值 → None

不支持 is_water_slide_mode_supported 的设备行为不变,例如 223 在这类设备上仍识别为 None。下发命令仍然发所选模式的标准代表值(扫拖/纯拖发 235,纯扫发 200),区间解析只用于读取。

兼容性与范围

公开的枚举值、选项列表、WATER_SLIDE_MODE_MAPPING、_WATER_SLIDE_RANGES 和面向用户的选项字符串都没有改,使用 mode.value 的下游无需迁移。本 PR 修的是组合模式的命令和识别路径,不是枚举别名本身的表示问题:WaterModes.PURE_WATER_FLOW_MIDDLE.code 和通用的 WaterModes.from_code_optional(235) 仍受影响。更大范围的"唯一值/显示名"迁移另行处理,所以本 PR 只引用、不关闭 #931。

不在本 PR 范围内:滑块设备上 custom/smart_mode 选项的暴露(#977)、其他协议族、Home Assistant 自动化策略、登录相关修复。

与进行中的 PR #983 的重叠: 两个 PR 都改了 get_water_slide_mode_name()。#983 加了一个 SMART_WATER 提前返回,本 PR 把这个函数的主体移进了 _resolve_water_slide_mode()。后合并的一方需要做一行文本冲突解决:在 helper 里返回 WaterModes.SMART_WATER,而不是 .value。如果 #983 先合并,我们可以基于它更新本分支。

复现与实机证据

在 G30S Ultra 上(Home Assistant 2026.9.1、python-roborock 7.2.3),真实状态里是 fan_power=102, water_box_mode=235, mop_mode=303。单项设置分别解析为 balanced/medium/deep_plus,但组合模式是 None(HA 显示 unknown)。一个等待确认 vac_and_mop 的自动化因此在发送启动命令前就停下了。

在本地应用同样的读写修复后,组合模式显示 vac_and_mop,下发的组合模式命令用的是 235,自动化通过了设置检查并真正开始清扫。直接读设备确认清扫进行中、无报错、电机设置符合预期,清扫计数也增加了。滑块中间位置这条路径在当前 main 上由单元测试覆盖,没有另外在实机上重跑。

验证

分支已与 main 的 6a0aab0543d6812c3eafa92571605444ab74cdb3 普通合并(没有 rebase,也没有 force-push)。

  • 回归 RED:用本 PR 的测试跑区间改动之前的生产代码,190 个测试中 69 个断言失败,全部是 7 个代表值之外的 23 个中间位置在三种风机设置下的 None == <CleaningMode...>,没有收集错误。只回退识别那一行,会复现同样的 69 个失败。
  • GREEN:uv run pytest tests/devices/traits/v1/test_status.py -q,190 个全部通过。
  • 全量 uv run pytest -q:Python 3.11 和 Python 3.14 上都是 1582 通过、53 个预期失败(xfail)、11 个按默认标记取消选择。同一个 main 提交不带本 PR:1462 通过、53 个 xfail。
  • uv run pre-commit run --all-files:通过(Ruff、Ruff format、Mypy、Codespell)。
  • uv build:源码包和 wheel 构建成功。

不包含任何设备或账号凭据、原始诊断数据或部署脚本。

@Lash-L

Lash-L commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Hi thanks for the fix! Can we take it a step further? Right now, this will still fail for say 223. Can we resolve the value via the range? When the user selects a value - we can set it to an exact value. But if the user changes it in the app to one of the inbetween slider values, we could still display the closest category.

e.g.

Add a small feature-gated range resolver

Preserve the raw value 223.

Resolve its range (221–223) to "slight".

@vize69

vize69 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Additional affected model: Qrevo Edge 2 Flow (roborock.vacuum.a328), python-roborock 7.12.0 as pinned by HA 2026.10.0b3.

An exact-source offline reproduction confirms ordinary slide water codes 225/235/248/250 cannot be classified through the original enum path. The feature-aware mapping approach used here also resolves those read cases in our isolated tests. We have not installed this PR or tested its write path on this robot, so this is not a full hardware validation of #961.

There is a separate issue on this device: it reports the valid custom triplet 106/204/302, with custom and smart feature flags enabled, but get_water_modes()'s slider early return omits the special modes. Consequently get_cleaning_mode_options() excludes custom, and HA displays unknown even though the decoder returns custom. Adding capability-gated special options restored custom readback locally. Upstream's existing test intentionally excludes these options, so the appropriate capability/model gate needs discussion rather than silently changing that assumption. Details: #977.

The independent archive-order issue is #976. Thanks for preserving existing UI strings and addressing both read and write mappings; these separate reports are not a claim that this PR fixes those additional cases.

Share the existing app range resolver between water-flow names and cleaning-mode classification. Preserve raw telemetry and the legacy fallback for non-slide codes, including custom and smart modes.

Cover every slider position across vacuum and mop fan settings, with capability gating and raw-value preservation checks.
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

@Lash-L thanks for pushing on this, and sorry for the slow turnaround. Done in 7e59415.

Since your comment, #975 landed the app's range table (_WATER_SLIDE_RANGES) and made water_mode_name range-based, so 223 already displays as "slight" there. The remaining gap was the combined cleaning mode: current_cleaning_mode still did an exact-code lookup, so 223 (or 248, after the representative codes moved) came back as None. This PR now classifies it through the same table:

  • One shared pure helper (_resolve_water_slide_mode) serves both get_water_slide_mode_name() and cleaning-mode classification, so the range boundaries are not duplicated. get_water_slide_mode_name() output is unchanged for every integer in range(-10, 400).
  • Gated on is_water_slide_mode_supported. Non-slide devices still return None for in-between codes.
  • The raw value is preserved: water_box_mode stays 223. Only the derived cleaning mode is resolved (223 → slight → vac_and_mop, or mop with the mop-only fan setting).
  • Writes are unchanged: selecting a mode still sends the exact representative code (235).
  • Non-range codes keep the previous fallback (204 → custom, 209 → smart_mode, unknown → None).

The tests cover every position from 221 to 250 across three fan settings. Without the change, 69 of them fail on in-between positions. Full suite: 1582 passed on Python 3.11 and 3.14 after merging current main.

One heads-up: #983 also edits get_water_slide_mode_name(), so whichever PR merges second needs a one-line conflict resolution. The details are in the PR description.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WaterModes pure-water members collapse into StrEnum aliases, sending water code 202 to water-slide devices (-10005 Param error)

3 participants