Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions roborock/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

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.device_features import DeviceFeatures
from roborock.devices.cache import Cache, CacheData
from roborock.devices.device import RoborockDevice
Expand Down Expand Up @@ -745,6 +746,21 @@ async def network_info(ctx, device_id: str):
await _display_v1_trait(context, device_id, lambda v1: v1.network_info)


def _parse_b01_q10_command(cmd: str) -> B01_Q10_DP:
"""Parse B01_Q10 command from either enum name or value."""
try:
return B01_Q10_DP(int(cmd))
except ValueError:
try:
return B01_Q10_DP.from_name(cmd)
except ValueError:
try:
return B01_Q10_DP.from_value(cmd)
except ValueError:
pass
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")


@click.command()
@click.option("--device_id", required=True)
@click.option("--cmd", required=True)
Expand All @@ -755,12 +771,18 @@ async def command(ctx, cmd, device_id, params):
context: RoborockContext = ctx.obj
device_manager = await context.get_device_manager()
device = await device_manager.get_device(device_id)
if device.v1_properties is None:
raise RoborockException(f"Device {device.name} does not support V1 protocol")
command_trait: Trait = device.v1_properties.command
result = await command_trait.send(cmd, json.loads(params) if params is not None else None)
if result:
click.echo(dump_json(result))
if device.v1_properties is not None:
command_trait: Trait = device.v1_properties.command
result = await command_trait.send(cmd, json.loads(params) if params is not None else None)
if result:
click.echo(dump_json(result))
elif device.b01_q10_properties is not None:
cmd_value = _parse_b01_q10_command(cmd)
command_trait: Trait = device.b01_q10_properties.command
await command_trait.send(cmd_value, json.loads(params) if params is not None else None)
click.echo("Command sent successfully; Enable debug logging (-d) to see responses.")
# Q10 commands don't have a specific time to respond, so wait a bit and log
await asyncio.sleep(5)


@click.command()
Expand Down
37 changes: 37 additions & 0 deletions roborock/devices/b01_q10_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Thin wrapper around the MQTT channel for Roborock B01 Q10 devices."""

from __future__ import annotations

import logging

from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
from roborock.exceptions import RoborockException
from roborock.protocols.b01_q10_protocol import (
ParamsType,
encode_mqtt_payload,
)

from .mqtt_channel import MqttChannel

_LOGGER = logging.getLogger(__name__)


async def send_command(
mqtt_channel: MqttChannel,
command: B01_Q10_DP,
params: ParamsType,
) -> None:
"""Send a command on the MQTT channel, without waiting for a response"""
_LOGGER.debug("Sending B01 MQTT command: cmd=%s params=%s", command, params)
roborock_message = encode_mqtt_payload(command, params)
_LOGGER.debug("Sending MQTT message: %s", roborock_message)
try:
await mqtt_channel.publish(roborock_message)
except RoborockException as ex:
_LOGGER.debug(
"Error sending B01 decoded command (method=%s params=%s): %s",
command,
params,
ex,
)
raise
2 changes: 1 addition & 1 deletion roborock/devices/b01_q7_channel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Thin wrapper around the MQTT channel for Roborock B01 devices."""
"""Thin wrapper around the MQTT channel for Roborock B01 Q7 devices."""

from __future__ import annotations

Expand Down
4 changes: 1 addition & 3 deletions roborock/devices/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
model_part = product.model.split(".")[-1]
if "ss" in model_part:
raise UnsupportedDeviceError(
f"Device {device.name} has unsupported version B01 product model {product.model}"
)
trait = b01.q10.create(channel)
elif "sc" in model_part:
# Q7 devices start with 'sc' in their model naming.
trait = b01.q7.create(channel)
Expand Down
9 changes: 8 additions & 1 deletion roborock/devices/traits/b01/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""Traits for B01 devices."""

from . import q7, q10
from .q7 import Q7PropertiesApi
from .q10 import Q10PropertiesApi

__all__ = ["Q7PropertiesApi", "q7", "q10"]
__all__ = [
"Q7PropertiesApi",
"Q10PropertiesApi",
"q7",
"q10",
]
30 changes: 29 additions & 1 deletion roborock/devices/traits/b01/q10/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
"""Q10"""
"""Traits for Q10 B01 devices."""

from typing import Any

from roborock.devices.b01_q7_channel import send_decoded_command
from roborock.devices.mqtt_channel import MqttChannel
from roborock.devices.traits import Trait

from .command import CommandTrait

__all__ = [
"Q10PropertiesApi",
]


class Q10PropertiesApi(Trait):
"""API for interacting with B01 devices."""

command: CommandTrait
"""Trait for sending commands to Q10 devices."""

def __init__(self, channel: MqttChannel) -> None:
"""Initialize the B01Props API."""
self.command = CommandTrait(channel)


def create(channel: MqttChannel) -> Q10PropertiesApi:
"""Create traits for B01 devices."""
return Q10PropertiesApi(channel)
32 changes: 32 additions & 0 deletions roborock/devices/traits/b01/q10/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Any

from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
from roborock.devices.b01_q10_channel import send_command
from roborock.devices.mqtt_channel import MqttChannel
from roborock.protocols.b01_q10_protocol import ParamsType


class CommandTrait:
"""Trait for sending commands to Q10 Roborock devices.

This trait allows sending raw commands directly to the device. It is particularly
useful for accessing features that do not have their own traits. Generally
it is preferred to use specific traits for device functionality when
available.
"""

def __init__(self, channel: MqttChannel) -> None:
"""Initialize the CommandTrait."""
self._channel = channel

async def send(self, command: B01_Q10_DP, params: ParamsType = None) -> Any:
"""Send a command to the device.

Sending a raw command to the device using this method does not update
the internal state of any other traits. It is the responsibility of the
caller to ensure that any traits affected by the command are refreshed
as needed.
"""
if not self._channel:
raise ValueError("Device trait in invalid state")
return await send_command(self._channel, command, params=params)
3 changes: 3 additions & 0 deletions roborock/devices/traits/traits_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class TraitsMixin:
b01_q7_properties: b01.Q7PropertiesApi | None = None
"""B01 Q7 properties trait, if supported."""

b01_q10_properties: b01.Q10PropertiesApi | None = None
"""B01 Q10 properties trait, if supported."""

def __init__(self, trait: Trait) -> None:
"""Initialize the TraitsMixin with the given trait.
Expand Down
5 changes: 2 additions & 3 deletions tests/data/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
HOME_DATA_RAW,
K_VALUE,
LOCAL_KEY,
PRODUCT_ID,
USER_DATA,
)

Expand Down Expand Up @@ -180,7 +179,7 @@ def test_home_data():
assert hd.lat is None
assert hd.geo_name is None
product = hd.products[0]
assert product.id == PRODUCT_ID
assert product.id == "product-id-s7-maxv"
assert product.name == "Roborock S7 MaxV"
assert product.code == "a27"
assert product.model == "roborock.vacuum.a27"
Expand All @@ -205,7 +204,7 @@ def test_home_data():
assert device.runtime_env is None
assert device.time_zone_id == "America/Los_Angeles"
assert device.icon_url == "no_url"
assert device.product_id == "product-id-123"
assert device.product_id == "product-id-s7-maxv"
assert device.lon is None
assert device.lat is None
assert not device.share
Expand Down
18 changes: 9 additions & 9 deletions tests/devices/__snapshots__/test_file_cache.ambr

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions tests/devices/__snapshots__/test_v1_device.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
'name': 'Roborock S7 MaxV',
'newFeatureSet': '0000000000002041',
'online': True,
'productId': 'product-id-123',
'productId': 'product-id-s7-maxv',
'pv': '1.0',
'roomId': 2362003,
'share': False,
Expand All @@ -40,7 +40,7 @@
'capability': 0,
'category': 'robot.vacuum.cleaner',
'code': 'a27',
'id': 'product-id-123',
'id': 'product-id-s7-maxv',
'model': 'roborock.vacuum.a27',
'name': 'Roborock S7 MaxV',
'schema': list([
Expand Down Expand Up @@ -453,7 +453,7 @@
'name': 'Roborock S7 MaxV',
'newFeatureSet': '0000000000002041',
'online': True,
'productId': 'product-id-123',
'productId': 'product-id-s7-maxv',
'pv': '1.0',
'roomId': 2362003,
'share': False,
Expand All @@ -466,7 +466,7 @@
'capability': 0,
'category': 'robot.vacuum.cleaner',
'code': 'a27',
'id': 'product-id-123',
'id': 'product-id-s7-maxv',
'model': 'roborock.vacuum.a27',
'name': 'Roborock S7 MaxV',
'schema': list([
Expand Down Expand Up @@ -859,7 +859,7 @@
'name': 'Roborock S7 MaxV',
'newFeatureSet': '0000000000002041',
'online': True,
'productId': 'product-id-123',
'productId': 'product-id-s7-maxv',
'pv': '1.0',
'roomId': 2362003,
'share': False,
Expand All @@ -872,7 +872,7 @@
'capability': 0,
'category': 'robot.vacuum.cleaner',
'code': 'a27',
'id': 'product-id-123',
'id': 'product-id-s7-maxv',
'model': 'roborock.vacuum.a27',
'name': 'Roborock S7 MaxV',
'schema': list([
Expand Down Expand Up @@ -1236,7 +1236,7 @@
'name': 'Roborock S7 MaxV',
'newFeatureSet': '0000000000002041',
'online': True,
'productId': 'product-id-123',
'productId': 'product-id-s7-maxv',
'pv': '1.0',
'roomId': 2362003,
'share': False,
Expand All @@ -1249,7 +1249,7 @@
'capability': 0,
'category': 'robot.vacuum.cleaner',
'code': 'a27',
'id': 'product-id-123',
'id': 'product-id-s7-maxv',
'model': 'roborock.vacuum.a27',
'name': 'Roborock S7 MaxV',
'schema': list([
Expand Down
22 changes: 22 additions & 0 deletions tests/e2e/__snapshots__/test_device_manager.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,28 @@
00000410 a0 9b 5e 72 8d 3e 57 69 0b 7c 21 80 2f a4 d5 12 |..^r.>Wi.|!./...|
00000420 99 be 49 6e f3 0b 57 e5 a8 1e 88 b6 7b 48 |..In..W.....{H|
# ---
# name: test_q10_device[home_data0]
[mqtt >]
00000000 10 29 00 04 4d 51 54 54 05 c2 00 3c 00 00 00 00 |.)..MQTT...<....|
00000010 08 31 39 36 34 38 66 39 34 00 10 32 33 34 36 37 |.19648f94..23467|
00000020 38 65 61 38 35 34 66 31 39 39 65 |8ea854f199e|
[mqtt <]
00000000 20 09 02 00 06 22 00 0a 21 00 14 | ...."..!..|
[mqtt >]
00000000 82 2e 00 01 00 00 28 72 72 2f 6d 2f 6f 2f 75 73 |......(rr/m/o/us|
00000010 65 72 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 |er123/19648f94/d|
00000020 65 76 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 |evice-id-def456.|
[mqtt <]
00000000 90 04 00 01 00 00 |......|
[mqtt >]
00000000 30 62 00 28 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0b.(rr/m/i/user1|
00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 69 |23/19648f94/devi|
00000020 63 65 2d 69 64 2d 64 65 66 34 35 36 00 42 30 31 |ce-id-def456.B01|
00000030 00 00 23 82 00 00 23 83 68 a6 a2 23 00 65 00 20 |..#...#.h..#.e. |
00000040 31 38 71 36 ad 3b 7d 9d 50 0b b6 f0 be 74 5d b9 |18q6.;}.P....t].|
00000050 7e 75 e3 ca e4 bc 42 34 f6 a5 2e ef c7 de 0c 10 |~u....B4........|
00000060 62 f0 6c f5 |b.l.|
# ---
# name: test_v1_device
[mqtt >]
00000000 10 29 00 04 4d 51 54 54 05 c2 00 3c 00 00 00 00 |.)..MQTT...<....|
Expand Down
Loading
Loading