Skip to content

Commit 6bc3458

Browse files
authored
feat: add support for getting and reseting consumables (#502)
* feat: add support for getting and reseting consumables * chore: add a class comment about availability * chore: remove whitespace
1 parent 70b78d2 commit 6bc3458

File tree

6 files changed

+165
-0
lines changed

6 files changed

+165
-0
lines changed

roborock/cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
roborock> status --device_id <device_id>
2222
```
2323
"""
24+
2425
import asyncio
2526
import datetime
2627
import functools
@@ -46,6 +47,7 @@
4647
from roborock.devices.device_manager import DeviceManager, create_device_manager, create_home_data_api
4748
from roborock.devices.traits import Trait
4849
from roborock.devices.traits.v1 import V1TraitMixin
50+
from roborock.devices.traits.v1.consumeable import ConsumableAttribute
4951
from roborock.protocol import MessageParser
5052
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
5153
from roborock.web_api import RoborockApiClient
@@ -449,6 +451,30 @@ async def maps(ctx, device_id: str):
449451
await _display_v1_trait(context, device_id, lambda v1: v1.maps)
450452

451453

454+
@session.command()
455+
@click.option("--device_id", required=True)
456+
@click.pass_context
457+
@async_command
458+
async def consumables(ctx, device_id: str):
459+
"""Get device consumables."""
460+
context: RoborockContext = ctx.obj
461+
await _display_v1_trait(context, device_id, lambda v1: v1.consumables)
462+
463+
464+
@session.command()
465+
@click.option("--device_id", required=True)
466+
@click.option("--consumable", required=True, type=click.Choice([e.value for e in ConsumableAttribute]))
467+
@click.pass_context
468+
@async_command
469+
async def reset_consumable(ctx, device_id: str, consumable: str):
470+
"""Reset a specific consumable attribute."""
471+
context: RoborockContext = ctx.obj
472+
trait = await _v1_trait(context, device_id, lambda v1: v1.consumables)
473+
attribute = ConsumableAttribute.from_str(consumable)
474+
await trait.reset_consumable(attribute)
475+
click.echo(f"Reset {consumable} for device {device_id}")
476+
477+
452478
@click.command()
453479
@click.option("--device_id", required=True)
454480
@click.option("--cmd", required=True)
@@ -691,6 +717,8 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur
691717
cli.add_command(volume)
692718
cli.add_command(set_volume)
693719
cli.add_command(maps)
720+
cli.add_command(consumables)
721+
cli.add_command(reset_consumable)
694722

695723

696724
def main():

roborock/devices/traits/v1/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .clean_summary import CleanSummaryTrait
1111
from .common import V1TraitMixin
12+
from .consumeable import ConsumableTrait
1213
from .do_not_disturb import DoNotDisturbTrait
1314
from .maps import MapsTrait
1415
from .status import StatusTrait
@@ -24,6 +25,7 @@
2425
"CleanSummaryTrait",
2526
"SoundVolumeTrait",
2627
"MapsTrait",
28+
"ConsumableTrait",
2729
]
2830

2931

@@ -40,6 +42,7 @@ class PropertiesApi(Trait):
4042
clean_summary: CleanSummaryTrait
4143
sound_volume: SoundVolumeTrait
4244
maps: MapsTrait
45+
consumables: ConsumableTrait
4346

4447
# In the future optional fields can be added below based on supported features
4548

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Trait for managing consumable attributes.
2+
3+
A consumable attribute is one that is expected to be replaced or refilled
4+
periodically, such as filters, brushes, etc.
5+
"""
6+
7+
from enum import StrEnum
8+
from typing import Self
9+
10+
from roborock.containers import Consumable
11+
from roborock.devices.traits.v1 import common
12+
from roborock.roborock_typing import RoborockCommand
13+
14+
__all__ = [
15+
"ConsumableTrait",
16+
]
17+
18+
19+
class ConsumableAttribute(StrEnum):
20+
"""Enum for consumable attributes."""
21+
22+
SENSOR_DIRTY_TIME = "sensor_dirty_time"
23+
FILTER_WORK_TIME = "filter_work_time"
24+
SIDE_BRUSH_WORK_TIME = "side_brush_work_time"
25+
MAIN_BRUSH_WORK_TIME = "main_brush_work_time"
26+
27+
@classmethod
28+
def from_str(cls, value: str) -> Self:
29+
"""Create a ConsumableAttribute from a string value."""
30+
for member in cls:
31+
if member.value == value:
32+
return member
33+
raise ValueError(f"Unknown ConsumableAttribute: {value}")
34+
35+
36+
class ConsumableTrait(Consumable, common.V1TraitMixin):
37+
"""Trait for managing consumable attributes on Roborock devices.
38+
39+
After the first refresh, you can tell what consumables are supported by
40+
checking which attributes are not None.
41+
"""
42+
43+
command = RoborockCommand.GET_CONSUMABLE
44+
45+
async def reset_consumable(self, consumable: ConsumableAttribute) -> None:
46+
"""Reset a specific consumable attribute on the device."""
47+
await self.rpc_channel.send_command(RoborockCommand.RESET_CONSUMABLE, params=[consumable.value])
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Tests for the DoNotDisturbTrait class."""
2+
3+
from unittest.mock import AsyncMock
4+
5+
import pytest
6+
7+
from roborock.devices.device import RoborockDevice
8+
from roborock.devices.traits.v1.consumeable import ConsumableAttribute, ConsumableTrait
9+
from roborock.roborock_typing import RoborockCommand
10+
11+
CONSUMABLE_DATA = [
12+
{
13+
"main_brush_work_time": 879348,
14+
"side_brush_work_time": 707618,
15+
"filter_work_time": 738722,
16+
"filter_element_work_time": 0,
17+
"sensor_dirty_time": 455517,
18+
}
19+
]
20+
21+
22+
@pytest.fixture
23+
def consumable_trait(device: RoborockDevice) -> ConsumableTrait:
24+
"""Create a ConsumableTrait instance with mocked dependencies."""
25+
assert device.v1_properties
26+
return device.v1_properties.consumables
27+
28+
29+
async def test_get_consumable_data_success(consumable_trait: ConsumableTrait, mock_rpc_channel: AsyncMock) -> None:
30+
"""Test successfully getting consumable data."""
31+
# Setup mock to return the sample consumable data
32+
mock_rpc_channel.send_command.return_value = CONSUMABLE_DATA
33+
34+
# Call the method
35+
await consumable_trait.refresh()
36+
# Verify the result
37+
assert consumable_trait.main_brush_work_time == 879348
38+
assert consumable_trait.side_brush_work_time == 707618
39+
assert consumable_trait.filter_work_time == 738722
40+
assert consumable_trait.filter_element_work_time == 0
41+
assert consumable_trait.sensor_dirty_time == 455517
42+
43+
# Verify the RPC call was made correctly
44+
mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_CONSUMABLE)
45+
46+
47+
@pytest.mark.parametrize(
48+
("consumable", "reset_param"),
49+
[
50+
(ConsumableAttribute.MAIN_BRUSH_WORK_TIME, "main_brush_work_time"),
51+
(ConsumableAttribute.SIDE_BRUSH_WORK_TIME, "side_brush_work_time"),
52+
(ConsumableAttribute.FILTER_WORK_TIME, "filter_work_time"),
53+
(ConsumableAttribute.SENSOR_DIRTY_TIME, "sensor_dirty_time"),
54+
],
55+
)
56+
async def test_reset_consumable_data(
57+
consumable_trait: ConsumableTrait,
58+
mock_rpc_channel: AsyncMock,
59+
consumable: ConsumableAttribute,
60+
reset_param: str,
61+
) -> None:
62+
"""Test successfully resetting consumable data."""
63+
# Call the method
64+
await consumable_trait.reset_consumable(consumable)
65+
66+
# Verify the RPC call was made correctly with expected parameters
67+
mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.RESET_CONSUMABLE, params=[reset_param])
68+
69+
70+
#

tests/protocols/__snapshots__/test_v1_protocol.ambr

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@
3333
]
3434
'''
3535
# ---
36+
# name: test_decode_rpc_payload[get_consumeables]
37+
20001
38+
# ---
39+
# name: test_decode_rpc_payload[get_consumeables].1
40+
'''
41+
[
42+
{
43+
"main_brush_work_time": 879348,
44+
"side_brush_work_time": 707618,
45+
"filter_work_time": 738722,
46+
"filter_element_work_time": 0,
47+
"sensor_dirty_time": 455517
48+
}
49+
]
50+
'''
51+
# ---
3652
# name: test_decode_rpc_payload[get_dnd]
3753
20002
3854
# ---
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"t":1759038395,"dps":{"102":"{\"id\":20001,\"result\":[{\"main_brush_work_time\":879348,\"side_brush_work_time\":707618,\"filter_work_time\":738722,\"filter_element_work_time\":0,\"sensor_dirty_time\":455517}]}"}}

0 commit comments

Comments
 (0)