diff --git a/roborock/devices/cache.py b/roborock/devices/cache.py index 7f82abaa..73a9bf8d 100644 --- a/roborock/devices/cache.py +++ b/roborock/devices/cache.py @@ -26,7 +26,13 @@ class CacheData: """Home map information indexed by map_flag.""" home_map_content: dict[int, bytes] = field(default_factory=dict) - """Home cache content for each map data indexed by map_flag.""" + """Home cache content for each map data indexed by map_flag. + + This is deprecated in favor of `home_map_content_base64`. + """ + + home_map_content_base64: dict[int, str] = field(default_factory=dict) + """Home cache content for each map data (encoded base64) indexed by map_flag.""" device_features: DeviceFeatures | None = None """Device features information.""" diff --git a/roborock/devices/traits/v1/home.py b/roborock/devices/traits/v1/home.py index 87d1604d..46541a44 100644 --- a/roborock/devices/traits/v1/home.py +++ b/roborock/devices/traits/v1/home.py @@ -16,6 +16,7 @@ """ import asyncio +import base64 import logging from typing import Self @@ -86,14 +87,20 @@ async def discover_home(self) -> None: After discovery, the home cache will be populated and can be accessed via the `home_map_info` property. """ cache_data = await self._cache.get() - if cache_data.home_map_info and cache_data.home_map_content: + if cache_data.home_map_info and (cache_data.home_map_content or cache_data.home_map_content_base64): _LOGGER.debug("Home cache already populated, skipping discovery") self._home_map_info = cache_data.home_map_info self._discovery_completed = True try: - self._home_map_content = { - k: self._map_content.parse_map_content(v) for k, v in cache_data.home_map_content.items() - } + if cache_data.home_map_content_base64: + self._home_map_content = { + k: self._map_content.parse_map_content(base64.b64decode(v)) + for k, v in cache_data.home_map_content_base64.items() + } + else: + self._home_map_content = { + k: self._map_content.parse_map_content(v) for k, v in cache_data.home_map_content.items() + } except (ValueError, RoborockException) as ex: _LOGGER.warning("Failed to parse cached home map content, will re-discover: %s", ex) self._home_map_content = {} @@ -218,7 +225,12 @@ async def _update_home_cache( """Update the entire home cache with new map info and content.""" cache_data = await self._cache.get() cache_data.home_map_info = home_map_info - cache_data.home_map_content = {k: v.raw_api_response for k, v in home_map_content.items() if v.raw_api_response} + cache_data.home_map_content_base64 = { + k: base64.b64encode(v.raw_api_response).decode("utf-8") + for k, v in home_map_content.items() + if v.raw_api_response + } + cache_data.home_map_content = {} await self._cache.set(cache_data) self._home_map_info = home_map_info self._home_map_content = home_map_content @@ -237,8 +249,18 @@ async def _update_current_map( if update_cache: cache_data = await self._cache.get() cache_data.home_map_info[map_flag] = map_info + # Migrate existing cached content to base64 if needed + if cache_data.home_map_content and not cache_data.home_map_content_base64: + cache_data.home_map_content_base64 = { + k: base64.b64encode(v).decode("utf-8") for k, v in cache_data.home_map_content.items() + } + cache_data.home_map_content = {} if map_content.raw_api_response: - cache_data.home_map_content[map_flag] = map_content.raw_api_response + if cache_data.home_map_content_base64 is None: + cache_data.home_map_content_base64 = {} + cache_data.home_map_content_base64[map_flag] = base64.b64encode(map_content.raw_api_response).decode( + "utf-8" + ) await self._cache.set(cache_data) if self._home_map_info is None: diff --git a/tests/devices/traits/v1/test_home.py b/tests/devices/traits/v1/test_home.py index f8a91148..6e8a57f9 100644 --- a/tests/devices/traits/v1/test_home.py +++ b/tests/devices/traits/v1/test_home.py @@ -1,5 +1,6 @@ """Tests for the Home related functionality.""" +import base64 from collections.abc import Iterator from unittest.mock import AsyncMock, MagicMock, patch @@ -7,7 +8,7 @@ from roborock.data.containers import CombinedMapInfo from roborock.data.v1.v1_code_mappings import RoborockStateCode -from roborock.devices.cache import InMemoryCache +from roborock.devices.cache import CacheData, InMemoryCache from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.home import HomeTrait from roborock.devices.traits.v1.map_content import MapContentTrait @@ -230,19 +231,32 @@ async def test_discover_home_empty_cache( # Verify the persistent cache has been updated cache_data = await home_trait._cache.get() assert len(cache_data.home_map_info) == 2 - assert len(cache_data.home_map_content) == 2 - - + assert len(cache_data.home_map_content_base64) == 2 + assert len(cache_data.home_map_content) == 0 + + +@pytest.mark.parametrize( + "cache_data", + [ + CacheData( + home_map_info={0: CombinedMapInfo(map_flag=0, name="Dummy", rooms=[])}, + home_map_content={0: MAP_BYTES_RESPONSE_1}, + ), + CacheData( + home_map_info={0: CombinedMapInfo(map_flag=0, name="Dummy", rooms=[])}, + home_map_content={}, + home_map_content_base64={0: base64.b64encode(MAP_BYTES_RESPONSE_1).decode("utf-8")}, + ), + ], +) async def test_discover_home_with_existing_cache( home_trait: HomeTrait, mock_rpc_channel: AsyncMock, mock_mqtt_rpc_channel: AsyncMock, + cache_data: CacheData, ) -> None: """Test that discovery is skipped when cache already exists.""" # Pre-populate the cache - cache_data = await home_trait._cache.get() - cache_data.home_map_info = {0: CombinedMapInfo(map_flag=0, name="Dummy", rooms=[])} - cache_data.home_map_content = {0: MAP_BYTES_RESPONSE_1} await home_trait._cache.set(cache_data) # Call discover_home @@ -507,7 +521,8 @@ async def test_discover_home_device_busy_cleaning( # Verify the persistent cache has been updated cache_data = await home_trait._cache.get() assert len(cache_data.home_map_info) == 2 - assert len(cache_data.home_map_content) == 2 + assert len(cache_data.home_map_content_base64) == 2 + assert len(cache_data.home_map_content) == 0 async def test_single_map_no_switching(