Skip to content

Commit 05c8e94

Browse files
allenporterCopilot
andauthored
feat: Add a trait for working with routines (#574)
* feat: Add a trait for working with routines This is re-exposing the web API as a trait, to make it similar to other device operations hiding the API implementation details. * chore: change imports for typing * chore: Update tests/devices/traits/v1/fixtures.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9f4e2c8 commit 05c8e94

File tree

7 files changed

+84
-0
lines changed

7 files changed

+84
-0
lines changed

roborock/devices/device_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
187187
channel.rpc_channel,
188188
channel.mqtt_rpc_channel,
189189
channel.map_rpc_channel,
190+
web_api,
190191
cache,
191192
map_parser_config=map_parser_config,
192193
)

roborock/devices/traits/v1/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from roborock.devices.traits import Trait
4141
from roborock.devices.v1_rpc_channel import V1RpcChannel
4242
from roborock.map.map_parser import MapParserConfig
43+
from roborock.web_api import UserWebApiClient
4344

4445
from .child_lock import ChildLockTrait
4546
from .clean_summary import CleanSummaryTrait
@@ -56,6 +57,7 @@
5657
from .maps import MapsTrait
5758
from .network_info import NetworkInfoTrait
5859
from .rooms import RoomsTrait
60+
from .routines import RoutinesTrait
5961
from .smart_wash_params import SmartWashParamsTrait
6062
from .status import StatusTrait
6163
from .valley_electricity_timer import ValleyElectricityTimerTrait
@@ -85,6 +87,7 @@
8587
"WashTowelModeTrait",
8688
"SmartWashParamsTrait",
8789
"NetworkInfoTrait",
90+
"RoutinesTrait",
8891
]
8992

9093

@@ -108,6 +111,7 @@ class PropertiesApi(Trait):
108111
home: HomeTrait
109112
device_features: DeviceFeaturesTrait
110113
network_info: NetworkInfoTrait
114+
routines: RoutinesTrait
111115

112116
# Optional features that may not be supported on all devices
113117
child_lock: ChildLockTrait | None = None
@@ -126,6 +130,7 @@ def __init__(
126130
rpc_channel: V1RpcChannel,
127131
mqtt_rpc_channel: V1RpcChannel,
128132
map_rpc_channel: V1RpcChannel,
133+
web_api: UserWebApiClient,
129134
cache: Cache,
130135
map_parser_config: MapParserConfig | None = None,
131136
) -> None:
@@ -134,6 +139,7 @@ def __init__(
134139
self._rpc_channel = rpc_channel
135140
self._mqtt_rpc_channel = mqtt_rpc_channel
136141
self._map_rpc_channel = map_rpc_channel
142+
self._web_api = web_api
137143
self._cache = cache
138144

139145
self.status = StatusTrait(product)
@@ -144,6 +150,7 @@ def __init__(
144150
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, cache)
145151
self.device_features = DeviceFeaturesTrait(product.product_nickname, cache)
146152
self.network_info = NetworkInfoTrait(device_uid, cache)
153+
self.routines = RoutinesTrait(device_uid, web_api)
147154

148155
# Dynamically create any traits that need to be populated
149156
for item in fields(self):
@@ -267,6 +274,7 @@ def create(
267274
rpc_channel: V1RpcChannel,
268275
mqtt_rpc_channel: V1RpcChannel,
269276
map_rpc_channel: V1RpcChannel,
277+
web_api: UserWebApiClient,
270278
cache: Cache,
271279
map_parser_config: MapParserConfig | None = None,
272280
) -> PropertiesApi:
@@ -278,6 +286,7 @@ def create(
278286
rpc_channel,
279287
mqtt_rpc_channel,
280288
map_rpc_channel,
289+
web_api,
281290
cache,
282291
map_parser_config,
283292
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Routines trait for V1 devices."""
2+
3+
from roborock.data.containers import HomeDataScene
4+
from roborock.web_api import UserWebApiClient
5+
6+
7+
class RoutinesTrait:
8+
"""Trait for interacting with routines."""
9+
10+
def __init__(self, device_id: str, web_api: UserWebApiClient) -> None:
11+
"""Initialize the routines trait."""
12+
self._device_id = device_id
13+
self._web_api = web_api
14+
15+
async def get_routines(self) -> list[HomeDataScene]:
16+
"""Get available routines."""
17+
return await self._web_api.get_routines(self._device_id)
18+
19+
async def execute_routine(self, routine_id: int) -> None:
20+
"""Execute a routine by its ID.
21+
22+
Technically, routines are per-device, but the API does not
23+
require the device ID to execute them. This can execute a
24+
routine for any device but it is exposed here for convenience.
25+
"""
26+
await self._web_api.execute_routine(routine_id)

roborock/web_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,11 @@ def __init__(self, web_api: RoborockApiClient, user_data: UserData) -> None:
725725
async def get_home_data(self) -> HomeData:
726726
"""Fetch home data using the API client."""
727727
return await self._web_api.get_home_data_v3(self._user_data)
728+
729+
async def get_routines(self, device_id: str) -> list[HomeDataScene]:
730+
"""Fetch routines (scenes) for a specific device."""
731+
return await self._web_api.get_scenes(self._user_data, device_id)
732+
733+
async def execute_routine(self, scene_id: int) -> None:
734+
"""Execute a specific routine (scene) by its ID."""
735+
await self._web_api.execute_scene(self._user_data, scene_id)

tests/devices/test_v1_device.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def device_fixture(channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel:
6262
rpc_channel,
6363
mqtt_rpc_channel,
6464
AsyncMock(),
65+
AsyncMock(),
6566
NoCache(),
6667
),
6768
)

tests/devices/traits/v1/fixtures.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def map_rpc_channel_fixture() -> AsyncMock:
4040
return AsyncMock()
4141

4242

43+
@pytest.fixture(autouse=True, name="web_api_client")
44+
def web_api_client_fixture() -> AsyncMock:
45+
"""Fixture to set up the web API client for tests."""
46+
return AsyncMock()
47+
48+
4349
@pytest.fixture(autouse=True, name="roborock_cache")
4450
def roborock_cache_fixture() -> Cache:
4551
"""Fixture to provide a NoCache instance for tests."""
@@ -52,6 +58,7 @@ def device_fixture(
5258
mock_rpc_channel: AsyncMock,
5359
mock_mqtt_rpc_channel: AsyncMock,
5460
mock_map_rpc_channel: AsyncMock,
61+
web_api_client: AsyncMock,
5562
roborock_cache: Cache,
5663
) -> RoborockDevice:
5764
"""Fixture to set up the device for tests."""
@@ -66,6 +73,7 @@ def device_fixture(
6673
mock_rpc_channel,
6774
mock_mqtt_rpc_channel,
6875
mock_map_rpc_channel,
76+
web_api_client,
6977
roborock_cache,
7078
),
7179
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Tests for the RoutinesTrait."""
2+
3+
from unittest.mock import AsyncMock
4+
5+
import pytest
6+
7+
from roborock.data.containers import HomeDataScene
8+
from roborock.devices.device import RoborockDevice
9+
from roborock.devices.traits.v1.routines import RoutinesTrait
10+
11+
12+
@pytest.fixture(name="routines_trait")
13+
def routines_trait_fixture(device: RoborockDevice) -> RoutinesTrait:
14+
"""Fixture for the routines trait."""
15+
assert device.v1_properties
16+
return device.v1_properties.routines
17+
18+
19+
async def test_get_routines(routines_trait: RoutinesTrait, web_api_client: AsyncMock) -> None:
20+
"""Test getting routines."""
21+
web_api_client.get_routines.return_value = [HomeDataScene(id=1, name="test_scene")]
22+
routines = await routines_trait.get_routines()
23+
assert len(routines) == 1
24+
assert routines[0].name == "test_scene"
25+
web_api_client.get_routines.assert_called_once()
26+
27+
28+
async def test_execute_routine(routines_trait: RoutinesTrait, web_api_client: AsyncMock) -> None:
29+
"""Test executing a routine."""
30+
await routines_trait.execute_routine(1)
31+
web_api_client.execute_routine.assert_called_once_with(1)

0 commit comments

Comments
 (0)