33import asyncio
44import datetime
55from collections .abc import Generator , Iterator
6+ from typing import Any
67from unittest .mock import AsyncMock , Mock , patch
78
89import 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+
175200async 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 ()
0 commit comments