WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 8d76119

Browse files
authored
feat: Recognize Q10 devices and add a command trait (#721)
* feat: Recognize Q10 devices and add a command trait Add the ability to send commands to roborock CLI. This adds a single trait for sending commands, using a blend of approaches from #692 and #709 * chore: Add end to end tests for Q10 devices Moves the mock API responses to json files to make them easier to collect for new device types and modify in tests. * chore: Remove unused timeout field
1 parent 7dc12c8 commit 8d76119

21 files changed

+1098
-589
lines changed

roborock/cli.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
from roborock import RoborockCommand
4545
from roborock.data import RoborockBase, UserData
46+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
4647
from roborock.device_features import DeviceFeatures
4748
from roborock.devices.cache import Cache, CacheData
4849
from roborock.devices.device import RoborockDevice
@@ -745,6 +746,21 @@ async def network_info(ctx, device_id: str):
745746
await _display_v1_trait(context, device_id, lambda v1: v1.network_info)
746747

747748

749+
def _parse_b01_q10_command(cmd: str) -> B01_Q10_DP:
750+
"""Parse B01_Q10 command from either enum name or value."""
751+
try:
752+
return B01_Q10_DP(int(cmd))
753+
except ValueError:
754+
try:
755+
return B01_Q10_DP.from_name(cmd)
756+
except ValueError:
757+
try:
758+
return B01_Q10_DP.from_value(cmd)
759+
except ValueError:
760+
pass
761+
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")
762+
763+
748764
@click.command()
749765
@click.option("--device_id", required=True)
750766
@click.option("--cmd", required=True)
@@ -755,12 +771,18 @@ async def command(ctx, cmd, device_id, params):
755771
context: RoborockContext = ctx.obj
756772
device_manager = await context.get_device_manager()
757773
device = await device_manager.get_device(device_id)
758-
if device.v1_properties is None:
759-
raise RoborockException(f"Device {device.name} does not support V1 protocol")
760-
command_trait: Trait = device.v1_properties.command
761-
result = await command_trait.send(cmd, json.loads(params) if params is not None else None)
762-
if result:
763-
click.echo(dump_json(result))
774+
if device.v1_properties is not None:
775+
command_trait: Trait = device.v1_properties.command
776+
result = await command_trait.send(cmd, json.loads(params) if params is not None else None)
777+
if result:
778+
click.echo(dump_json(result))
779+
elif device.b01_q10_properties is not None:
780+
cmd_value = _parse_b01_q10_command(cmd)
781+
command_trait: Trait = device.b01_q10_properties.command
782+
await command_trait.send(cmd_value, json.loads(params) if params is not None else None)
783+
click.echo("Command sent successfully; Enable debug logging (-d) to see responses.")
784+
# Q10 commands don't have a specific time to respond, so wait a bit and log
785+
await asyncio.sleep(5)
764786

765787

766788
@click.command()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Thin wrapper around the MQTT channel for Roborock B01 Q10 devices."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
8+
from roborock.exceptions import RoborockException
9+
from roborock.protocols.b01_q10_protocol import (
10+
ParamsType,
11+
encode_mqtt_payload,
12+
)
13+
14+
from .mqtt_channel import MqttChannel
15+
16+
_LOGGER = logging.getLogger(__name__)
17+
18+
19+
async def send_command(
20+
mqtt_channel: MqttChannel,
21+
command: B01_Q10_DP,
22+
params: ParamsType,
23+
) -> None:
24+
"""Send a command on the MQTT channel, without waiting for a response"""
25+
_LOGGER.debug("Sending B01 MQTT command: cmd=%s params=%s", command, params)
26+
roborock_message = encode_mqtt_payload(command, params)
27+
_LOGGER.debug("Sending MQTT message: %s", roborock_message)
28+
try:
29+
await mqtt_channel.publish(roborock_message)
30+
except RoborockException as ex:
31+
_LOGGER.debug(
32+
"Error sending B01 decoded command (method=%s params=%s): %s",
33+
command,
34+
params,
35+
ex,
36+
)
37+
raise

roborock/devices/b01_q7_channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Thin wrapper around the MQTT channel for Roborock B01 devices."""
1+
"""Thin wrapper around the MQTT channel for Roborock B01 Q7 devices."""
22

33
from __future__ import annotations
44

roborock/devices/device_manager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
242242
channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
243243
model_part = product.model.split(".")[-1]
244244
if "ss" in model_part:
245-
raise UnsupportedDeviceError(
246-
f"Device {device.name} has unsupported version B01 product model {product.model}"
247-
)
245+
trait = b01.q10.create(channel)
248246
elif "sc" in model_part:
249247
# Q7 devices start with 'sc' in their model naming.
250248
trait = b01.q7.create(channel)
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"""Traits for B01 devices."""
22

3+
from . import q7, q10
34
from .q7 import Q7PropertiesApi
5+
from .q10 import Q10PropertiesApi
46

5-
__all__ = ["Q7PropertiesApi", "q7", "q10"]
7+
__all__ = [
8+
"Q7PropertiesApi",
9+
"Q10PropertiesApi",
10+
"q7",
11+
"q10",
12+
]
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
1-
"""Q10"""
1+
"""Traits for Q10 B01 devices."""
2+
3+
from typing import Any
4+
5+
from roborock.devices.b01_q7_channel import send_decoded_command
6+
from roborock.devices.mqtt_channel import MqttChannel
7+
from roborock.devices.traits import Trait
8+
9+
from .command import CommandTrait
10+
11+
__all__ = [
12+
"Q10PropertiesApi",
13+
]
14+
15+
16+
class Q10PropertiesApi(Trait):
17+
"""API for interacting with B01 devices."""
18+
19+
command: CommandTrait
20+
"""Trait for sending commands to Q10 devices."""
21+
22+
def __init__(self, channel: MqttChannel) -> None:
23+
"""Initialize the B01Props API."""
24+
self.command = CommandTrait(channel)
25+
26+
27+
def create(channel: MqttChannel) -> Q10PropertiesApi:
28+
"""Create traits for B01 devices."""
29+
return Q10PropertiesApi(channel)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Any
2+
3+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
4+
from roborock.devices.b01_q10_channel import send_command
5+
from roborock.devices.mqtt_channel import MqttChannel
6+
from roborock.protocols.b01_q10_protocol import ParamsType
7+
8+
9+
class CommandTrait:
10+
"""Trait for sending commands to Q10 Roborock devices.
11+
12+
This trait allows sending raw commands directly to the device. It is particularly
13+
useful for accessing features that do not have their own traits. Generally
14+
it is preferred to use specific traits for device functionality when
15+
available.
16+
"""
17+
18+
def __init__(self, channel: MqttChannel) -> None:
19+
"""Initialize the CommandTrait."""
20+
self._channel = channel
21+
22+
async def send(self, command: B01_Q10_DP, params: ParamsType = None) -> Any:
23+
"""Send a command to the device.
24+
25+
Sending a raw command to the device using this method does not update
26+
the internal state of any other traits. It is the responsibility of the
27+
caller to ensure that any traits affected by the command are refreshed
28+
as needed.
29+
"""
30+
if not self._channel:
31+
raise ValueError("Device trait in invalid state")
32+
return await send_command(self._channel, command, params=params)

roborock/devices/traits/traits_mixin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class TraitsMixin:
3434
b01_q7_properties: b01.Q7PropertiesApi | None = None
3535
"""B01 Q7 properties trait, if supported."""
3636

37+
b01_q10_properties: b01.Q10PropertiesApi | None = None
38+
"""B01 Q10 properties trait, if supported."""
39+
3740
def __init__(self, trait: Trait) -> None:
3841
"""Initialize the TraitsMixin with the given trait.
3942

tests/data/test_containers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
HOME_DATA_RAW,
2020
K_VALUE,
2121
LOCAL_KEY,
22-
PRODUCT_ID,
2322
USER_DATA,
2423
)
2524

@@ -180,7 +179,7 @@ def test_home_data():
180179
assert hd.lat is None
181180
assert hd.geo_name is None
182181
product = hd.products[0]
183-
assert product.id == PRODUCT_ID
182+
assert product.id == "product-id-s7-maxv"
184183
assert product.name == "Roborock S7 MaxV"
185184
assert product.code == "a27"
186185
assert product.model == "roborock.vacuum.a27"
@@ -205,7 +204,7 @@ def test_home_data():
205204
assert device.runtime_env is None
206205
assert device.time_zone_id == "America/Los_Angeles"
207206
assert device.icon_url == "no_url"
208-
assert device.product_id == "product-id-123"
207+
assert device.product_id == "product-id-s7-maxv"
209208
assert device.lon is None
210209
assert device.lat is None
211210
assert not device.share

tests/devices/__snapshots__/test_file_cache.ambr

Lines changed: 9 additions & 9 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)