Skip to content

Commit 8a0ec4c

Browse files
committed
feat: Allow device manager to perform rediscovery of devices
This adds an opton to skip the cache, but also supports fallback to the old device list. Adds tests exercising redisocvery and various cache falblack cases on failure.
1 parent 2b21387 commit 8a0ec4c

File tree

3 files changed

+109
-8
lines changed

3 files changed

+109
-8
lines changed

roborock/devices/device_manager.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
UserData,
1616
)
1717
from roborock.devices.device import DeviceReadyCallback, RoborockDevice
18+
from roborock.exceptions import RoborockException
1819
from roborock.map.map_parser import MapParserConfig
1920
from roborock.mqtt.roborock_session import create_lazy_mqtt_session
2021
from roborock.mqtt.session import MqttSession
@@ -68,12 +69,17 @@ def __init__(
6869
self._devices: dict[str, RoborockDevice] = {}
6970
self._mqtt_session = mqtt_session
7071

71-
async def discover_devices(self) -> list[RoborockDevice]:
72+
async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevice]:
7273
"""Discover all devices for the logged-in user."""
7374
cache_data = await self._cache.get()
74-
if not cache_data.home_data:
75-
_LOGGER.debug("No cached home data found, fetching from API")
76-
cache_data.home_data = await self._web_api.get_home_data()
75+
if not cache_data.home_data or not prefer_cache:
76+
_LOGGER.debug("Fetching home data (prefer_cache=%s)", prefer_cache)
77+
try:
78+
cache_data.home_data = await self._web_api.get_home_data()
79+
except RoborockException as ex:
80+
if not cache_data.home_data:
81+
raise ex
82+
_LOGGER.debug("Failed to fetch home data, using cached data: %s", ex)
7783
await self._cache.set(cache_data)
7884
home_data = cache_data.home_data
7985

tests/devices/test_device_manager.py

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import datetime
55
from collections.abc import Generator, Iterator
6+
from typing import Any
67
from unittest.mock import AsyncMock, Mock, patch
78

89
import pytest
@@ -143,7 +144,8 @@ async def test_create_home_data_api_exception() -> None:
143144
await api.get_home_data()
144145

145146

146-
async def test_cache_logic() -> None:
147+
@pytest.mark.parametrize(("prefer_cache", "expected_call_count"), [(True, 1), (False, 2)])
148+
async def test_cache_logic(prefer_cache: bool, expected_call_count: int) -> None:
147149
"""Test that the cache logic works correctly."""
148150
call_count = 0
149151

@@ -161,8 +163,8 @@ async def mock_home_data_with_counter(*args, **kwargs) -> HomeData:
161163
assert call_count == 1
162164

163165
# Second call should use cache, not increment call_count
164-
devices2 = await device_manager.discover_devices()
165-
assert call_count == 1 # Should still be 1, not 2
166+
devices2 = await device_manager.discover_devices(prefer_cache=prefer_cache)
167+
assert call_count == expected_call_count
166168
assert len(devices2) == 1
167169

168170
await device_manager.close()
@@ -172,6 +174,29 @@ async def mock_home_data_with_counter(*args, **kwargs) -> HomeData:
172174
await device_manager.close()
173175

174176

177+
async def test_home_data_api_fails_with_cache_fallback() -> None:
178+
"""Test that home data exceptions may still fall back to use the cache when available."""
179+
180+
cache = InMemoryCache()
181+
cache_data = await cache.get()
182+
cache_data.home_data = HomeData.from_dict(mock_data.HOME_DATA_RAW)
183+
await cache.set(cache_data)
184+
185+
with patch(
186+
"roborock.devices.device_manager.RoborockApiClient.get_home_data_v3",
187+
side_effect=RoborockException("Test exception"),
188+
):
189+
# This call will skip the API and use the cache
190+
device_manager = await create_device_manager(USER_PARAMS, cache=cache)
191+
192+
# This call will hit the API since we're not preferring the cache
193+
# but will fallback to the cache data on exception
194+
devices2 = await device_manager.discover_devices(prefer_cache=False)
195+
assert len(devices2) == 1
196+
197+
await device_manager.close()
198+
199+
175200
async def test_ready_callback(home_data: HomeData) -> None:
176201
"""Test that the ready callback is invoked when a device connects."""
177202
ready_devices: list[RoborockDevice] = []
@@ -231,3 +256,72 @@ async def test_start_connect_failure(home_data: HomeData, channel_failure: Mock,
231256

232257
await device_manager.close()
233258
assert mock_unsub.call_count == 1
259+
260+
261+
async def test_rediscover_devices(mock_rpc_channel: AsyncMock) -> None:
262+
"""Test that we can discover devices multiple times and discovery new devices."""
263+
raw_devices: list[dict[str, Any]] = mock_data.HOME_DATA_RAW["devices"]
264+
assert len(raw_devices) > 0
265+
raw_device_1 = raw_devices[0]
266+
267+
home_data_responses = [
268+
HomeData.from_dict(mock_data.HOME_DATA_RAW),
269+
# New device added on second call. We make a copy and updated fields to simulate
270+
# a new device.
271+
HomeData.from_dict(
272+
{
273+
**mock_data.HOME_DATA_RAW,
274+
"devices": [
275+
raw_device_1,
276+
{
277+
**raw_device_1,
278+
"duid": "new_device_duid",
279+
"name": "New Device",
280+
"model": "roborock.newmodel.v1",
281+
"mac": "00:11:22:33:44:55",
282+
},
283+
],
284+
}
285+
),
286+
]
287+
288+
mock_rpc_channel.send_command.side_effect = [
289+
[mock_data.APP_GET_INIT_STATUS],
290+
mock_data.STATUS,
291+
# Device #2
292+
[mock_data.APP_GET_INIT_STATUS],
293+
mock_data.STATUS,
294+
]
295+
296+
async def mock_home_data_with_counter(*args, **kwargs) -> HomeData:
297+
nonlocal home_data_responses
298+
return home_data_responses.pop(0)
299+
300+
# First call happens during create_device_manager initialization
301+
with patch(
302+
"roborock.devices.device_manager.RoborockApiClient.get_home_data_v3",
303+
side_effect=mock_home_data_with_counter,
304+
):
305+
device_manager = await create_device_manager(USER_PARAMS, cache=InMemoryCache())
306+
assert len(await device_manager.get_devices()) == 1
307+
308+
# Second call should use cache and does not add new device
309+
await device_manager.discover_devices(prefer_cache=True)
310+
assert len(await device_manager.get_devices()) == 1
311+
312+
# Third call should fetch new home data and add the new device
313+
await device_manager.discover_devices(prefer_cache=False)
314+
assert len(await device_manager.get_devices()) == 2
315+
316+
# Verify the two devices exist with correct data
317+
device_1 = await device_manager.get_device("abc123")
318+
assert device_1 is not None
319+
assert device_1.name == "Roborock S7 MaxV"
320+
321+
new_device = await device_manager.get_device("new_device_duid")
322+
assert new_device
323+
assert new_device is not None
324+
assert new_device.name == "New Device"
325+
326+
# Ensure closing again works without error
327+
await device_manager.close()

tests/mock_data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import hashlib
44
import json
5+
from typing import Any
56

67
# All data is based on a U.S. customer with a Roborock S7 MaxV Ultra
78
USER_EMAIL = "user@domain.com"
@@ -119,7 +120,7 @@
119120
"type": "WORKFLOW",
120121
}
121122
]
122-
HOME_DATA_RAW = {
123+
HOME_DATA_RAW: dict[str, Any] = {
123124
"id": 123456,
124125
"name": "My Home",
125126
"lon": None,

0 commit comments

Comments
 (0)