Skip to content

Commit e504a08

Browse files
committed
fix: Improve Home trait discovery process.
This updates the Home trait to allow the current map to be refreshed even if initial discovery fails. The caller can invoke discovery at startup, but then after just invoke refresh and the right thing should happen. We now track discovery explicitly and only persist the cache when we have completed discovery to avoid persisting an incomplete view of all available maps.
1 parent c5cf8c2 commit e504a08

File tree

2 files changed

+128
-24
lines changed

2 files changed

+128
-24
lines changed

roborock/devices/traits/v1/home.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
This trait depends on the MapsTrait and RoomsTrait to gather the necessary
88
information. It provides properties to access the current map, the list of
99
rooms with names, and the map image and data.
10+
11+
Callers may first call `discover_home()` to populate the home layout cache by
12+
iterating through all available maps on the device. This will cache the map
13+
information and room names for all maps to minimize map switching and improve
14+
performance. After the initial discovery, callers can call `refresh()` to update
15+
the current map's information and room names as needed.
1016
"""
1117

1218
import asyncio
@@ -64,6 +70,7 @@ def __init__(
6470
self._map_content = map_content
6571
self._rooms_trait = rooms_trait
6672
self._cache = cache
73+
self._discovery_completed = False
6774
self._home_map_info: dict[int, CombinedMapInfo] | None = None
6875
self._home_map_content: dict[int, MapContent] | None = None
6976

@@ -82,6 +89,7 @@ async def discover_home(self) -> None:
8289
if cache_data.home_map_info and cache_data.home_map_content:
8390
_LOGGER.debug("Home cache already populated, skipping discovery")
8491
self._home_map_info = cache_data.home_map_info
92+
self._discovery_completed = True
8593
try:
8694
self._home_map_content = {
8795
k: self._map_content.parse_map_content(v) for k, v in cache_data.home_map_content.items()
@@ -101,6 +109,7 @@ async def discover_home(self) -> None:
101109

102110
home_map_info, home_map_content = await self._build_home_map_info()
103111
_LOGGER.debug("Home discovery complete, caching data for %d maps", len(home_map_info))
112+
self._discovery_completed = True
104113
await self._update_home_cache(home_map_info, home_map_content)
105114

106115
async def _refresh_map_info(self, map_info) -> CombinedMapInfo:
@@ -156,8 +165,15 @@ async def refresh(self) -> Self:
156165
active maps or re-discover the home. It is expected that this will keep
157166
information up to date for the current map as users switch to that map.
158167
"""
159-
if self._home_map_info is None:
160-
raise RoborockException("Cannot refresh home data without home cache, did you call discover_home()?")
168+
if not self._discovery_completed:
169+
# Running initial discovery also populates all of the same information
170+
# as below so we can just call that method. If the device is busy
171+
# then we'll fall through below to refresh the current map only.
172+
try:
173+
await self.discover_home()
174+
return self
175+
except RoborockDeviceBusy:
176+
_LOGGER.debug("Cannot refresh home data while device is busy cleaning")
161177

162178
# Refresh the list of map names/info
163179
await self._maps_trait.refresh()
@@ -170,7 +186,9 @@ async def refresh(self) -> Self:
170186
new_map_content = await self._refresh_map_content()
171187
# Refresh the current map's room data
172188
combined_map_info = await self._refresh_map_info(current_map_info)
173-
await self._update_current_map_cache(map_flag, combined_map_info, new_map_content)
189+
await self._update_current_map(
190+
map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
191+
)
174192
return self
175193

176194
@property
@@ -206,16 +224,28 @@ async def _update_home_cache(
206224
self._home_map_info = home_map_info
207225
self._home_map_content = home_map_content
208226

209-
async def _update_current_map_cache(
210-
self, map_flag: int, map_info: CombinedMapInfo, map_content: MapContent
227+
async def _update_current_map(
228+
self,
229+
map_flag: int,
230+
map_info: CombinedMapInfo,
231+
map_content: MapContent,
232+
update_cache: bool,
211233
) -> None:
212234
"""Update the cache for the current map only."""
213-
cache_data = await self._cache.get()
214-
cache_data.home_map_info[map_flag] = map_info
215-
if map_content.raw_api_response:
216-
cache_data.home_map_content[map_flag] = map_content.raw_api_response
217-
await self._cache.set(cache_data)
218-
if self._home_map_info is None or self._home_map_content is None:
219-
raise RoborockException("Home cache is not initialized, cannot update current map cache")
235+
# Update the persistent cache if requested e.g. home discovery has
236+
# completed and we want to keep it fresh. Otherwise just update the
237+
# in memory map below.
238+
if update_cache:
239+
cache_data = await self._cache.get()
240+
cache_data.home_map_info[map_flag] = map_info
241+
if map_content.raw_api_response:
242+
cache_data.home_map_content[map_flag] = map_content.raw_api_response
243+
await self._cache.set(cache_data)
244+
245+
if self._home_map_info is None:
246+
self._home_map_info = {}
220247
self._home_map_info[map_flag] = map_info
248+
249+
if self._home_map_content is None:
250+
self._home_map_content = {}
221251
self._home_map_content[map_flag] = map_content

tests/devices/traits/v1/test_home.py

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ async def test_discover_home_empty_cache(
227227
assert current_map_data.map_flag == 0
228228
assert current_map_data.name == "Ground Floor"
229229

230+
# Verify the persistent cache has been updated
231+
cache_data = await home_trait._cache.get()
232+
assert len(cache_data.home_map_info) == 2
233+
assert len(cache_data.home_map_content) == 2
234+
230235

231236
async def test_discover_home_with_existing_cache(
232237
home_trait: HomeTrait,
@@ -372,17 +377,6 @@ async def test_refresh_updates_current_map_cache(
372377
assert home_trait.home_map_content[0].image_content == TEST_IMAGE_CONTENT_1
373378

374379

375-
async def test_refresh_no_cache_no_update(
376-
home_trait: HomeTrait,
377-
mock_rpc_channel: AsyncMock,
378-
mock_mqtt_rpc_channel: AsyncMock,
379-
) -> None:
380-
"""Test that refresh doesn't update when no cache exists."""
381-
# Perform refresh without existing cache
382-
with pytest.raises(Exception, match="Cannot refresh home data without home cache, did you call discover_home()?"):
383-
await home_trait.refresh()
384-
385-
386380
async def test_current_map_data_property(
387381
home_trait: HomeTrait,
388382
mock_rpc_channel: AsyncMock,
@@ -425,8 +419,14 @@ async def test_discover_home_device_busy_cleaning(
425419
home_trait: HomeTrait,
426420
mock_rpc_channel: AsyncMock,
427421
mock_mqtt_rpc_channel: AsyncMock,
422+
mock_map_rpc_channel: AsyncMock,
428423
) -> None:
429-
"""Test that discovery raises RoborockDeviceBusy when device is cleaning."""
424+
"""Test that discovery raises RoborockDeviceBusy when device is cleaning.
425+
426+
This tests the initial failure scenario during discovery where the device
427+
is busy, then a retry attepmt where the device is still busy, then finally
428+
a successful attempt to run discovery when the device is idle.
429+
"""
430430
# Set the status trait state to cleaning
431431
status_trait.state = RoborockStateCode.cleaning
432432

@@ -438,6 +438,80 @@ async def test_discover_home_device_busy_cleaning(
438438
assert mock_rpc_channel.send_command.call_count == 0
439439
assert mock_mqtt_rpc_channel.send_command.call_count == 0
440440

441+
# Setup mocks for refresh
442+
mock_rpc_channel.send_command.side_effect = [
443+
ROOM_MAPPING_DATA_MAP_0, # Room mapping refresh
444+
]
445+
mock_mqtt_rpc_channel.send_command.side_effect = [
446+
MULTI_MAP_LIST_DATA, # Maps refresh
447+
]
448+
mock_map_rpc_channel.send_command.side_effect = [
449+
MAP_BYTES_RESPONSE_1, # Map bytes refresh
450+
]
451+
452+
# Now attempt to refresh the device while cleaning. This should still fail
453+
# home discovery but allow refresh to update the current map.
454+
await home_trait.refresh()
455+
456+
# Verify the home information is now populated
457+
current_data = home_trait.current_map_data
458+
assert current_data is not None
459+
assert current_data.map_flag == 0
460+
assert current_data.name == "Ground Floor"
461+
assert home_trait.home_map_info is not None
462+
assert home_trait.home_map_info.keys() == {0}
463+
assert home_trait.home_map_content is not None
464+
assert home_trait.home_map_content.keys() == {0}
465+
map_0_content = home_trait.home_map_content[0]
466+
assert map_0_content is not None
467+
assert map_0_content.image_content == TEST_IMAGE_CONTENT_1
468+
assert map_0_content.map_data is not None
469+
470+
# Verify the persistent cache has not been updated since discovery
471+
# has not fully completed.
472+
cache_data = await home_trait._cache.get()
473+
assert not cache_data.home_map_info
474+
assert not cache_data.home_map_content
475+
476+
# Set the status trait state to idle which will mean we can attempt discovery
477+
# on the next refresh. This should have the result of updating the
478+
# persistent cache which is verified below.
479+
status_trait.state = RoborockStateCode.idle
480+
481+
# Setup mocks for the discovery process
482+
mock_rpc_channel.send_command.side_effect = [
483+
UPDATED_STATUS_MAP_123, # Status after switching to map 123
484+
ROOM_MAPPING_DATA_MAP_123, # Rooms for map 123
485+
UPDATED_STATUS_MAP_0, # Status after switching back to map 0
486+
ROOM_MAPPING_DATA_MAP_0, # Rooms for map 0
487+
]
488+
mock_mqtt_rpc_channel.send_command.side_effect = [
489+
MULTI_MAP_LIST_DATA, # Multi maps list
490+
{}, # LOAD_MULTI_MAP response for map 123
491+
{}, # LOAD_MULTI_MAP response back to map 0
492+
]
493+
mock_map_rpc_channel.send_command.side_effect = [
494+
MAP_BYTES_RESPONSE_2, # Map bytes for 123
495+
MAP_BYTES_RESPONSE_1, # Map bytes for 0
496+
]
497+
498+
# Refreshing should now perform discovery successfully
499+
await home_trait.refresh()
500+
501+
# Verify we now have all of the information populated from discovery
502+
assert home_trait.home_map_info is not None
503+
assert len(home_trait.home_map_info) == 2
504+
505+
assert home_trait.home_map_info is not None
506+
assert home_trait.home_map_info.keys() == {0, 123}
507+
assert home_trait.home_map_content is not None
508+
assert home_trait.home_map_content.keys() == {0, 123}
509+
510+
# Verify the persistent cache has been updated
511+
cache_data = await home_trait._cache.get()
512+
assert len(cache_data.home_map_info) == 2
513+
assert len(cache_data.home_map_content) == 2
514+
441515

442516
async def test_single_map_no_switching(
443517
home_trait: HomeTrait,

0 commit comments

Comments
 (0)