From cc6b2024918351d8f36302384a62a5b59016d81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 13 Feb 2026 22:18:38 +0100 Subject: [PATCH 1/9] feat(q10): add Roborock Q10 S5+ support with CLI commands Add vacuum trait methods (empty_dustbin, set_clean_mode, set_fan_level), dedicated Q10 CLI session commands, and ROBOROCK_Q10 constant. Co-Authored-By: Claude Opus 4.6 --- roborock/cli.py | 160 +++++++++++++++++++++- roborock/const.py | 2 + roborock/devices/traits/b01/q10/vacuum.py | 27 +++- 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/roborock/cli.py b/roborock/cli.py index 9f02b2de..9b66bcd1 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -43,13 +43,14 @@ from roborock import RoborockCommand from roborock.data import RoborockBase, UserData -from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP +from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXCleanType, YXFanLevel from roborock.data.code_mappings import SHORT_MODEL_TO_ENUM from roborock.device_features import DeviceFeatures from roborock.devices.cache import Cache, CacheData from roborock.devices.device import RoborockDevice from roborock.devices.device_manager import DeviceManager, UserParams, create_device_manager from roborock.devices.traits import Trait +from roborock.devices.traits.b01.q10.vacuum import VacuumTrait from roborock.devices.traits.v1 import V1TraitMixin from roborock.devices.traits.v1.consumeable import ConsumableAttribute from roborock.devices.traits.v1.map_content import MapContentTrait @@ -438,6 +439,15 @@ async def _display_v1_trait(context: RoborockContext, device_id: str, display_fu click.echo(dump_json(trait.as_dict())) +async def _q10_vacuum_trait(context: RoborockContext, device_id: str) -> VacuumTrait: + """Get VacuumTrait from Q10 device.""" + device_manager = await context.get_device_manager() + device = await device_manager.get_device(device_id) + if device.b01_q10_properties is None: + raise RoborockUnsupportedFeature("Device does not support B01 Q10 protocol. Is it a Q10?") + return device.b01_q10_properties.vacuum + + @session.command() @click.option("--device_id", required=True) @click.pass_context @@ -1172,6 +1182,154 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur cli.add_command(network_info) +# --- Q10 session commands --- + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_vacuum_start(ctx: click.Context, device_id: str) -> None: + """Start vacuum cleaning on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.start_clean() + click.echo("Starting vacuum cleaning...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_vacuum_pause(ctx: click.Context, device_id: str) -> None: + """Pause vacuum cleaning on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.pause_clean() + click.echo("Pausing vacuum cleaning...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_vacuum_resume(ctx: click.Context, device_id: str) -> None: + """Resume vacuum cleaning on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.resume_clean() + click.echo("Resuming vacuum cleaning...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_vacuum_stop(ctx: click.Context, device_id: str) -> None: + """Stop vacuum cleaning on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.stop_clean() + click.echo("Stopping vacuum cleaning...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_vacuum_dock(ctx: click.Context, device_id: str) -> None: + """Return vacuum to dock on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.return_to_dock() + click.echo("Returning vacuum to dock...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.pass_context +@async_command +async def q10_empty_dustbin(ctx: click.Context, device_id: str) -> None: + """Empty the dustbin at the dock on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + await trait.empty_dustbin() + click.echo("Emptying dustbin...") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.option("--mode", required=True, type=click.Choice(["bothwork", "onlysweep", "onlymop"]), help="Clean mode") +@click.pass_context +@async_command +async def q10_set_clean_mode(ctx: click.Context, device_id: str, mode: str) -> None: + """Set the cleaning mode on Q10 device (vacuum, mop, or both).""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + clean_mode = YXCleanType.from_value(mode) + await trait.set_clean_mode(clean_mode) + click.echo(f"Clean mode set to {mode}") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + +@session.command() +@click.option("--device_id", required=True, help="Device ID") +@click.option( + "--level", + required=True, + type=click.Choice(["close", "quite", "normal", "strong", "max", "super"]), + help="Fan suction level", +) +@click.pass_context +@async_command +async def q10_set_fan_level(ctx: click.Context, device_id: str, level: str) -> None: + """Set the fan suction level on Q10 device.""" + context: RoborockContext = ctx.obj + try: + trait = await _q10_vacuum_trait(context, device_id) + fan_level = YXFanLevel.from_value(level) + await trait.set_fan_level(fan_level) + click.echo(f"Fan level set to {level}") + except RoborockUnsupportedFeature: + click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") + except RoborockException as e: + click.echo(f"Error: {e}") + + def main(): return cli() diff --git a/roborock/const.py b/roborock/const.py index 510c745e..643d5570 100644 --- a/roborock/const.py +++ b/roborock/const.py @@ -33,6 +33,7 @@ ROBOROCK_QREVO_MASTER = "roborock.vacuum.a117" ROBOROCK_QREVO_CURV = "roborock.vacuum.a135" ROBOROCK_Q8_MAX = "roborock.vacuum.a73" +ROBOROCK_Q10 = "roborock.vacuum.ss07" ROBOROCK_G10S_PRO = "roborock.vacuum.a26" ROBOROCK_G20S_Ultra = "roborock.vacuum.a143" # cn saros_r10 ROBOROCK_G10S = "roborock.vacuum.a46" @@ -76,6 +77,7 @@ ROBOROCK_S4_MAX, ROBOROCK_S7, ROBOROCK_P10, + ROBOROCK_Q10, ROCKROBO_G10_SG, ] diff --git a/roborock/devices/traits/b01/q10/vacuum.py b/roborock/devices/traits/b01/q10/vacuum.py index 1b7104c6..7b5e2b64 100644 --- a/roborock/devices/traits/b01/q10/vacuum.py +++ b/roborock/devices/traits/b01/q10/vacuum.py @@ -1,6 +1,10 @@ """Traits for Q10 B01 devices.""" -from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP +from roborock.data.b01_q10.b01_q10_code_mappings import ( + B01_Q10_DP, + YXCleanType, + YXFanLevel, +) from .command import CommandTrait @@ -54,3 +58,24 @@ async def return_to_dock(self) -> None: command=B01_Q10_DP.START_DOCK_TASK, params={}, ) + + async def empty_dustbin(self) -> None: + """Empty the dustbin at the dock.""" + await self._command.send( + command=B01_Q10_DP.START_DOCK_TASK, + params=2, + ) + + async def set_clean_mode(self, mode: YXCleanType) -> None: + """Set the cleaning mode (vacuum, mop, or both).""" + await self._command.send( + command=B01_Q10_DP.CLEAN_MODE, + params=mode.code, + ) + + async def set_fan_level(self, level: YXFanLevel) -> None: + """Set the fan suction level.""" + await self._command.send( + command=B01_Q10_DP.FAN_LEVEL, + params=level.code, + ) From dc96bd8a41ae8ebf0cae36a65566ea515932f452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 13 Feb 2026 22:29:26 +0100 Subject: [PATCH 2/9] docs: update DEVICES.md with Q7/Q10 B01 device references Replace generic "Some newer models" with explicit Q7/Q10 series mentions across protocol tables, diagrams, and code organization. Co-Authored-By: Claude Opus 4.6 --- docs/DEVICES.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/DEVICES.md b/docs/DEVICES.md index 39ad775e..c3e726b9 100644 --- a/docs/DEVICES.md +++ b/docs/DEVICES.md @@ -15,6 +15,10 @@ Cloud and Network. * **Washers (A01)**: Use `device.a01_properties` for Dyad/Zeo devices. * Use `await device.a01_properties.query_values([...])` to get state. * Use `await device.a01_properties.set_value(protocol, value)` to control. + * **Vacuums (B01 Q10)**: Use `device.b01_q10_properties` for Q10 series devices. + * Use `device.b01_q10_properties.vacuum` to access vacuum commands (start, pause, stop, dock, empty dustbin, set clean mode, set fan level). + * Use `device.b01_q10_properties.command.send()` for raw DP commands. + * **Vacuums (B01 Q7)**: Use `device.b01_q7_properties` for Q7 series devices. ## Background: Understanding Device Protocols @@ -26,7 +30,7 @@ Cloud and Network. |----------|----------------|------|-----------|--------------|-------| | **V1** (`pv=1.0`) | Most vacuum robots (S7, S8, Q5, Q7, etc.) | ✅ | ✅ | `V1Channel` with `RpcChannel` | Prefers local, falls back to MQTT | | **A01** (`pv=A01`) | Dyad, Zeo washers | ✅ | ❌ | `MqttChannel` + helpers | MQTT only, DPS protocol | -| **B01** (`pv=B01`) | Some newer models | ✅ | ❌ | `MqttChannel` + helpers | MQTT only, DPS protocol | +| **B01** (`pv=B01`) | Q7, Q10 series | ✅ | ❌ | `MqttChannel` + helpers | MQTT only, DPS protocol | **Key Point:** The `DeviceManager` automatically detects the protocol version and creates the appropriate channel type. You don't need to handle this manually. @@ -47,7 +51,7 @@ graph TB subgraph "Device Types by Protocol" V1Dev[V1 Devices
pv=1.0
Most vacuums] A01Dev[A01 Devices
pv=A01
Dyad, Zeo] - B01Dev[B01 Devices
pv=B01
Some models] + B01Dev[B01 Devices
pv=B01
Q7, Q10 series] end subgraph "Traits Layer" @@ -148,7 +152,7 @@ graph TB |----------|-------------|---------------|--------------|----------| | **V1** (`pv=1.0`) | `V1Channel` with `RpcChannel` | ✅ Yes | Multi-strategy (Local → MQTT) | Most vacuum robots | | **A01** (`pv=A01`) | `MqttChannel` + helpers | ❌ No | Direct MQTT | Dyad, Zeo washers | -| **B01** (`pv=B01`) | `MqttChannel` + helpers | ❌ No | Direct MQTT | Some newer models | +| **B01** (`pv=B01`) | `MqttChannel` + helpers | ❌ No | Direct MQTT | Q7, Q10 series | ## Account Setup Internals @@ -249,7 +253,7 @@ sequenceDiagram RPC-->>App: Status ``` -#### A01/B01 Devices (Dyad, Zeo) - MQTT Only +#### A01/B01 Devices (Dyad, Zeo, Q7, Q10) - MQTT Only ```mermaid sequenceDiagram @@ -302,7 +306,7 @@ sequenceDiagram | **Local Support** | ✅ Yes, preferred | ❌ No | | **Fallback** | Local → MQTT | N/A | | **Connection** | Requires network info fetch | Direct MQTT | -| **Examples** | Most vacuum robots | Dyad washers, Zeo models | +| **Examples** | Most vacuum robots | Dyad washers, Zeo, Q7, Q10 | ### MQTT Connection (All Devices) @@ -510,7 +514,7 @@ Different device models use different protocol versions: |----------|---------|----------| | V1 | Most vacuum robots | JSON RPC with AES encryption | | A01 | Dyad, Zeo | DPS-based protocol | -| B01 | Some newer models | DPS-based protocol | +| B01 | Q7, Q10 series | DPS-based protocol | | L01 | Local protocol variant | Binary protocol negotiation | The protocol layer handles encoding/decoding transparently based on the device's `pv` field. @@ -577,11 +581,14 @@ roborock/ │ | ├── b01_q10_channel.py # B01 Q10 protocol helpers │ | └── ... │ └── traits/ # High-level device-specific command traits -│ └── v1/ # V1 device traits -│ ├── __init__.py # Trait initialization -│ ├── clean.py # Cleaning commands -│ ├── map.py # Map management -│ └── ... +│ ├── v1/ # V1 device traits +│ │ ├── __init__.py # Trait initialization +│ │ ├── clean.py # Cleaning commands +│ │ ├── map.py # Map management +│ │ └── ... +│ └── b01/ # B01 device traits +│ ├── q10/ # Q10 series (vacuum, command) +│ └── q7/ # Q7 series ├── mqtt/ # MQTT session management │ ├── session.py # Base session interface │ └── roborock_session.py # MQTT session with idle timeout From 2a50dc250cc5a2c51cfad2258ec761e2b5d572fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 13 Feb 2026 22:41:40 +0100 Subject: [PATCH 3/9] Update roborock/devices/traits/b01/q10/vacuum.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- roborock/devices/traits/b01/q10/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/devices/traits/b01/q10/vacuum.py b/roborock/devices/traits/b01/q10/vacuum.py index 7b5e2b64..1ed9febb 100644 --- a/roborock/devices/traits/b01/q10/vacuum.py +++ b/roborock/devices/traits/b01/q10/vacuum.py @@ -63,7 +63,7 @@ async def empty_dustbin(self) -> None: """Empty the dustbin at the dock.""" await self._command.send( command=B01_Q10_DP.START_DOCK_TASK, - params=2, + params=2, # 2 = dock task type for "empty dustbin" ) async def set_clean_mode(self, mode: YXCleanType) -> None: From d903112e230ce4952d1a311e64caa67d711cf192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 10:12:29 +0100 Subject: [PATCH 4/9] fix: correct typo in fan level and add normalization method --- roborock/data/b01_q10/b01_q10_code_mappings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 8330862f..d64c0c89 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -121,12 +121,17 @@ class B01_Q10_DP(RoborockModeEnum): class YXFanLevel(RoborockModeEnum): UNKNOWN = "unknown", -1 CLOSE = "close", 0 - QUITE = "quite", 1 + QUIET = "quiet", 1 NORMAL = "normal", 2 STRONG = "strong", 3 MAX = "max", 4 SUPER = "super", 8 + @classmethod + def from_value(cls, value: str) -> "YXFanLevel": + normalized = "quiet" if value.lower() == "quite" else value + return super().from_value(normalized) + class YXWaterLevel(RoborockModeEnum): UNKNOWN = "unknown", -1 From 0e8480acdeb0cf1f7db4b48a0ba9b3bca9bc0698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 10:12:46 +0100 Subject: [PATCH 5/9] fix: correct typo in fan level option and improve help message --- roborock/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roborock/cli.py b/roborock/cli.py index 9b66bcd1..5343fe69 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -1311,8 +1311,8 @@ async def q10_set_clean_mode(ctx: click.Context, device_id: str, mode: str) -> N @click.option( "--level", required=True, - type=click.Choice(["close", "quite", "normal", "strong", "max", "super"]), - help="Fan suction level", + type=click.Choice(["close", "quiet", "quite", "normal", "strong", "max", "super"]), + help='Fan suction level (one of "close", "quiet", "normal", "strong", "max", "super")', ) @click.pass_context @async_command @@ -1323,7 +1323,7 @@ async def q10_set_fan_level(ctx: click.Context, device_id: str, level: str) -> N trait = await _q10_vacuum_trait(context, device_id) fan_level = YXFanLevel.from_value(level) await trait.set_fan_level(fan_level) - click.echo(f"Fan level set to {level}") + click.echo(f"Fan level set to {fan_level.value}") except RoborockUnsupportedFeature: click.echo("Device does not support B01 Q10 protocol. Is it a Q10?") except RoborockException as e: From a48c6ef4301ffaddde6c19276c91aed0a4813c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 10:13:22 +0100 Subject: [PATCH 6/9] feat(test): add additional vacuum command tests for dustbin emptying and cleaning modes --- tests/devices/traits/b01/q10/test_vacuum.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/devices/traits/b01/q10/test_vacuum.py b/tests/devices/traits/b01/q10/test_vacuum.py index 394dc55a..c8bdb3a4 100644 --- a/tests/devices/traits/b01/q10/test_vacuum.py +++ b/tests/devices/traits/b01/q10/test_vacuum.py @@ -4,6 +4,7 @@ import pytest +from roborock.data.b01_q10.b01_q10_code_mappings import YXCleanType, YXFanLevel from roborock.devices.traits.b01.q10 import Q10PropertiesApi from roborock.devices.traits.b01.q10.vacuum import VacuumTrait from tests.fixtures.channel_fixtures import FakeChannel @@ -32,6 +33,9 @@ def vacuumm_fixture(q10_api: Q10PropertiesApi) -> VacuumTrait: (lambda x: x.resume_clean(), {"205": {}}), (lambda x: x.stop_clean(), {"206": {}}), (lambda x: x.return_to_dock(), {"203": {}}), + (lambda x: x.empty_dustbin(), {"203": 2}), + (lambda x: x.set_clean_mode(YXCleanType.BOTH_WORK), {"137": 1}), + (lambda x: x.set_fan_level(YXFanLevel.NORMAL), {"123": 2}), ], ) async def test_vacuum_commands( From 0e57ece24eaedbd76150ac688119e242562769b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 10:13:53 +0100 Subject: [PATCH 7/9] feat(tests): add conftest.py for test configuration and path setup --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f96b4d86 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) From ce7383a69c731b15d49d8b132cc5e2817ca4dfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 10:25:04 +0100 Subject: [PATCH 8/9] refactor(tests): remove unused conftest.py configuration file --- tests/conftest.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index f96b4d86..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) From 494a752365ac83c654e8825ac2c7045a50d99ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 14 Feb 2026 13:28:39 +0100 Subject: [PATCH 9/9] fix: remove normalization method for fan level option and update CLI choices --- roborock/cli.py | 2 +- roborock/data/b01_q10/b01_q10_code_mappings.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/roborock/cli.py b/roborock/cli.py index 5343fe69..8e53f5d1 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -1311,7 +1311,7 @@ async def q10_set_clean_mode(ctx: click.Context, device_id: str, mode: str) -> N @click.option( "--level", required=True, - type=click.Choice(["close", "quiet", "quite", "normal", "strong", "max", "super"]), + type=click.Choice(["close", "quiet", "normal", "strong", "max", "super"]), help='Fan suction level (one of "close", "quiet", "normal", "strong", "max", "super")', ) @click.pass_context diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index d64c0c89..abadb9de 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -127,11 +127,6 @@ class YXFanLevel(RoborockModeEnum): MAX = "max", 4 SUPER = "super", 8 - @classmethod - def from_value(cls, value: str) -> "YXFanLevel": - normalized = "quiet" if value.lower() == "quite" else value - return super().from_value(normalized) - class YXWaterLevel(RoborockModeEnum): UNKNOWN = "unknown", -1