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
diff --git a/roborock/cli.py b/roborock/cli.py
index 9f02b2de..8e53f5d1 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", "quiet", "normal", "strong", "max", "super"]),
+ help='Fan suction level (one of "close", "quiet", "normal", "strong", "max", "super")',
+)
+@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 {fan_level.value}")
+ 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/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py
index 8330862f..abadb9de 100644
--- a/roborock/data/b01_q10/b01_q10_code_mappings.py
+++ b/roborock/data/b01_q10/b01_q10_code_mappings.py
@@ -121,7 +121,7 @@ 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
diff --git a/roborock/devices/traits/b01/q10/vacuum.py b/roborock/devices/traits/b01/q10/vacuum.py
index 1b7104c6..1ed9febb 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, # 2 = dock task type for "empty dustbin"
+ )
+
+ 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,
+ )
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(