roborock.devices.traits.v1.home

Trait that represents a full view of the home layout.

This trait combines information about maps and rooms to provide a comprehensive view of the home layout, including room names and their corresponding segment on the map. It also makes it straight forward to fetch the map image and data.

This trait depends on the MapsTrait and RoomsTrait to gather the necessary information. It provides properties to access the current map, the list of rooms with names, and the map image and data.

Callers may first call discover_home() to populate the home layout cache by iterating through all available maps on the device. This will cache the map information and room names for all maps to minimize map switching and improve performance. After the initial discovery, callers can call refresh() to update the current map's information and room names as needed.

  1"""Trait that represents a full view of the home layout.
  2
  3This trait combines information about maps and rooms to provide a comprehensive
  4view of the home layout, including room names and their corresponding segment
  5on the map. It also makes it straight forward to fetch the map image and data.
  6
  7This trait depends on the MapsTrait and RoomsTrait to gather the necessary
  8information. It provides properties to access the current map, the list of
  9rooms with names, and the map image and data.
 10
 11Callers may first call `discover_home()` to populate the home layout cache by
 12iterating through all available maps on the device. This will cache the map
 13information and room names for all maps to minimize map switching and improve
 14performance. After the initial discovery, callers can call `refresh()` to update
 15the current map's information and room names as needed.
 16"""
 17
 18import asyncio
 19import base64
 20import logging
 21from typing import Self
 22
 23from roborock.data import CombinedMapInfo, NamedRoomMapping, RoborockBase
 24from roborock.data.v1.v1_code_mappings import RoborockStateCode
 25from roborock.devices.cache import DeviceCache
 26from roborock.devices.traits.v1 import common
 27from roborock.exceptions import RoborockDeviceBusy, RoborockException, RoborockInvalidStatus
 28from roborock.roborock_typing import RoborockCommand
 29
 30from .map_content import MapContent, MapContentTrait
 31from .maps import MapsTrait
 32from .rooms import RoomsTrait
 33from .status import StatusTrait
 34
 35_LOGGER = logging.getLogger(__name__)
 36
 37MAP_SLEEP = 3
 38
 39
 40class HomeTrait(RoborockBase, common.V1TraitMixin):
 41    """Trait that represents a full view of the home layout."""
 42
 43    command = RoborockCommand.GET_MAP_V1  # This is not used
 44
 45    def __init__(
 46        self,
 47        status_trait: StatusTrait,
 48        maps_trait: MapsTrait,
 49        map_content: MapContentTrait,
 50        rooms_trait: RoomsTrait,
 51        device_cache: DeviceCache,
 52    ) -> None:
 53        """Initialize the HomeTrait.
 54
 55        We keep track of the MapsTrait and RoomsTrait to provide a comprehensive
 56        view of the home layout. This also depends on the StatusTrait to determine
 57        the current map. See comments in MapsTrait for details on that dependency.
 58
 59        The cache is used to store discovered home data to minimize map switching
 60        and improve performance. The cache should be persisted by the caller to
 61        ensure data is retained across restarts.
 62
 63        After initial discovery, only information for the current map is refreshed
 64        to keep data up to date without excessive map switching. However, as
 65        users switch rooms, the current map's data will be updated to ensure
 66        accuracy.
 67        """
 68        super().__init__()
 69        self._status_trait = status_trait
 70        self._maps_trait = maps_trait
 71        self._map_content = map_content
 72        self._rooms_trait = rooms_trait
 73        self._device_cache = device_cache
 74        self._discovery_completed = False
 75        self._home_map_info: dict[int, CombinedMapInfo] | None = None
 76        self._home_map_content: dict[int, MapContent] | None = None
 77
 78    async def discover_home(self) -> None:
 79        """Iterate through all maps to discover rooms and cache them.
 80
 81        This will be a no-op if the home cache is already populated.
 82
 83        This cannot be called while the device is cleaning, as that would interrupt the
 84        cleaning process. This will raise `RoborockDeviceBusy` if the device is
 85        currently cleaning.
 86
 87        After discovery, the home cache will be populated and can be accessed via the `home_map_info` property.
 88        """
 89        device_cache_data = await self._device_cache.get()
 90        if device_cache_data and device_cache_data.home_map_info:
 91            _LOGGER.debug("Home cache already populated, skipping discovery")
 92            self._home_map_info = device_cache_data.home_map_info
 93            self._discovery_completed = True
 94            try:
 95                self._home_map_content = {
 96                    k: self._map_content.parse_map_content(base64.b64decode(v))
 97                    for k, v in (device_cache_data.home_map_content_base64 or {}).items()
 98                }
 99            except (ValueError, RoborockException) as ex:
100                _LOGGER.warning("Failed to parse cached home map content, will re-discover: %s", ex)
101                self._home_map_content = {}
102            else:
103                return
104
105        if self._status_trait.state == RoborockStateCode.cleaning:
106            raise RoborockDeviceBusy("Cannot perform home discovery while the device is cleaning")
107
108        await self._maps_trait.refresh()
109        if self._maps_trait.current_map_info is None:
110            raise RoborockException("Cannot perform home discovery without current map info")
111
112        home_map_info, home_map_content = await self._build_home_map_info()
113        _LOGGER.debug("Home discovery complete, caching data for %d maps", len(home_map_info))
114        self._discovery_completed = True
115        await self._update_home_cache(home_map_info, home_map_content)
116
117    async def _refresh_map_info(self, map_info) -> CombinedMapInfo:
118        """Collect room data for a specific map and return CombinedMapInfo."""
119        await self._rooms_trait.refresh()
120
121        rooms: dict[int, NamedRoomMapping] = {}
122        if map_info.rooms:
123            # Not all vacuums resopnd with rooms inside map_info.
124            for room in map_info.rooms:
125                if room.id is not None and room.iot_name_id is not None:
126                    rooms[room.id] = NamedRoomMapping(
127                        segment_id=room.id,
128                        iot_id=room.iot_name_id,
129                        name=room.iot_name or "Unknown",
130                    )
131
132        # Add rooms from rooms_trait. If room already exists and rooms_trait has "Unknown", don't override.
133        if self._rooms_trait.rooms:
134            for room in self._rooms_trait.rooms:
135                if room.segment_id is not None and room.name:
136                    if room.segment_id not in rooms or room.name != "Unknown":
137                        # Add the room to rooms if the room segment is not already in it
138                        # or if the room name isn't unknown.
139                        rooms[room.segment_id] = room
140
141        return CombinedMapInfo(
142            map_flag=map_info.map_flag,
143            name=map_info.name,
144            rooms=list(rooms.values()),
145        )
146
147    async def _refresh_map_content(self) -> MapContent:
148        """Refresh the map content trait to get the latest map data."""
149        await self._map_content.refresh()
150        return MapContent(
151            image_content=self._map_content.image_content,
152            map_data=self._map_content.map_data,
153            raw_api_response=self._map_content.raw_api_response,
154        )
155
156    async def _build_home_map_info(self) -> tuple[dict[int, CombinedMapInfo], dict[int, MapContent]]:
157        """Perform the actual discovery and caching of home map info and content."""
158        home_map_info: dict[int, CombinedMapInfo] = {}
159        home_map_content: dict[int, MapContent] = {}
160
161        # Sort map_info to process the current map last, reducing map switching.
162        # False (non-original maps) sorts before True (original map). We ensure
163        # we load the original map last.
164        sorted_map_infos = sorted(
165            self._maps_trait.map_info or [],
166            key=lambda mi: mi.map_flag == self._maps_trait.current_map,
167            reverse=False,
168        )
169        _LOGGER.debug("Building home cache for maps: %s", [mi.map_flag for mi in sorted_map_infos])
170        for map_info in sorted_map_infos:
171            # We need to load each map to get its room data
172            if len(sorted_map_infos) > 1:
173                _LOGGER.debug("Loading map %s", map_info.map_flag)
174                try:
175                    await self._maps_trait.set_current_map(map_info.map_flag)
176                except RoborockInvalidStatus as ex:
177                    # Device is in a state that forbids map switching. Translate to
178                    # "busy" so callers can fall back to refreshing the current map only.
179                    raise RoborockDeviceBusy("Cannot switch maps right now (device action locked)") from ex
180                await asyncio.sleep(MAP_SLEEP)
181
182            map_content = await self._refresh_map_content()
183            home_map_content[map_info.map_flag] = map_content
184
185            combined_map_info = await self._refresh_map_info(map_info)
186            home_map_info[map_info.map_flag] = combined_map_info
187        return home_map_info, home_map_content
188
189    async def refresh(self) -> None:
190        """Refresh current map's underlying map and room data, updating cache as needed.
191
192        This will only refresh the current map's data and will not populate non
193        active maps or re-discover the home. It is expected that this will keep
194        information up to date for the current map as users switch to that map.
195        """
196        if not self._discovery_completed:
197            # Running initial discovery also populates all of the same information
198            # as below so we can just call that method. If the device is busy
199            # then we'll fall through below to refresh the current map only.
200            try:
201                await self.discover_home()
202                return
203            except RoborockDeviceBusy:
204                _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
205
206        # Refresh the list of map names/info
207        await self._maps_trait.refresh()
208        if (current_map_info := self._maps_trait.current_map_info) is None or (
209            map_flag := self._maps_trait.current_map
210        ) is None:
211            raise RoborockException("Cannot refresh home data without current map info")
212
213        # Refresh the map content to ensure we have the latest image and object positions
214        new_map_content = await self._refresh_map_content()
215        # Refresh the current map's room data
216        combined_map_info = await self._refresh_map_info(current_map_info)
217        await self._update_current_map(
218            map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
219        )
220
221    @property
222    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
223        """Returns the map information for all cached maps."""
224        return self._home_map_info
225
226    @property
227    def current_map_data(self) -> CombinedMapInfo | None:
228        """Returns the map data for the current map."""
229        current_map_flag = self._maps_trait.current_map
230        if current_map_flag is None or self._home_map_info is None:
231            return None
232        return self._home_map_info.get(current_map_flag)
233
234    @property
235    def home_map_content(self) -> dict[int, MapContent] | None:
236        """Returns the map content for all cached maps."""
237        return self._home_map_content
238
239    def _parse_response(self, response: common.V1ResponseData) -> Self:
240        """This trait does not parse responses directly."""
241        raise NotImplementedError("HomeTrait does not support direct command responses")
242
243    async def _update_home_cache(
244        self, home_map_info: dict[int, CombinedMapInfo], home_map_content: dict[int, MapContent]
245    ) -> None:
246        """Update the entire home cache with new map info and content."""
247        device_cache_data = await self._device_cache.get()
248        device_cache_data.home_map_info = home_map_info
249        device_cache_data.home_map_content_base64 = {
250            k: base64.b64encode(v.raw_api_response).decode("utf-8")
251            for k, v in home_map_content.items()
252            if v.raw_api_response
253        }
254        await self._device_cache.set(device_cache_data)
255        self._home_map_info = home_map_info
256        self._home_map_content = home_map_content
257
258    async def _update_current_map(
259        self,
260        map_flag: int,
261        map_info: CombinedMapInfo,
262        map_content: MapContent,
263        update_cache: bool,
264    ) -> None:
265        """Update the cache for the current map only."""
266        # Update the persistent cache if requested e.g. home discovery has
267        # completed and we want to keep it fresh. Otherwise just update the
268        # in memory map below.
269        if update_cache:
270            device_cache_data = await self._device_cache.get()
271            if device_cache_data.home_map_info is None:
272                device_cache_data.home_map_info = {}
273            device_cache_data.home_map_info[map_flag] = map_info
274            if map_content.raw_api_response:
275                if device_cache_data.home_map_content_base64 is None:
276                    device_cache_data.home_map_content_base64 = {}
277                device_cache_data.home_map_content_base64[map_flag] = base64.b64encode(
278                    map_content.raw_api_response
279                ).decode("utf-8")
280            await self._device_cache.set(device_cache_data)
281
282        if self._home_map_info is None:
283            self._home_map_info = {}
284        self._home_map_info[map_flag] = map_info
285
286        if self._home_map_content is None:
287            self._home_map_content = {}
288        self._home_map_content[map_flag] = map_content
MAP_SLEEP = 3
 41class HomeTrait(RoborockBase, common.V1TraitMixin):
 42    """Trait that represents a full view of the home layout."""
 43
 44    command = RoborockCommand.GET_MAP_V1  # This is not used
 45
 46    def __init__(
 47        self,
 48        status_trait: StatusTrait,
 49        maps_trait: MapsTrait,
 50        map_content: MapContentTrait,
 51        rooms_trait: RoomsTrait,
 52        device_cache: DeviceCache,
 53    ) -> None:
 54        """Initialize the HomeTrait.
 55
 56        We keep track of the MapsTrait and RoomsTrait to provide a comprehensive
 57        view of the home layout. This also depends on the StatusTrait to determine
 58        the current map. See comments in MapsTrait for details on that dependency.
 59
 60        The cache is used to store discovered home data to minimize map switching
 61        and improve performance. The cache should be persisted by the caller to
 62        ensure data is retained across restarts.
 63
 64        After initial discovery, only information for the current map is refreshed
 65        to keep data up to date without excessive map switching. However, as
 66        users switch rooms, the current map's data will be updated to ensure
 67        accuracy.
 68        """
 69        super().__init__()
 70        self._status_trait = status_trait
 71        self._maps_trait = maps_trait
 72        self._map_content = map_content
 73        self._rooms_trait = rooms_trait
 74        self._device_cache = device_cache
 75        self._discovery_completed = False
 76        self._home_map_info: dict[int, CombinedMapInfo] | None = None
 77        self._home_map_content: dict[int, MapContent] | None = None
 78
 79    async def discover_home(self) -> None:
 80        """Iterate through all maps to discover rooms and cache them.
 81
 82        This will be a no-op if the home cache is already populated.
 83
 84        This cannot be called while the device is cleaning, as that would interrupt the
 85        cleaning process. This will raise `RoborockDeviceBusy` if the device is
 86        currently cleaning.
 87
 88        After discovery, the home cache will be populated and can be accessed via the `home_map_info` property.
 89        """
 90        device_cache_data = await self._device_cache.get()
 91        if device_cache_data and device_cache_data.home_map_info:
 92            _LOGGER.debug("Home cache already populated, skipping discovery")
 93            self._home_map_info = device_cache_data.home_map_info
 94            self._discovery_completed = True
 95            try:
 96                self._home_map_content = {
 97                    k: self._map_content.parse_map_content(base64.b64decode(v))
 98                    for k, v in (device_cache_data.home_map_content_base64 or {}).items()
 99                }
100            except (ValueError, RoborockException) as ex:
101                _LOGGER.warning("Failed to parse cached home map content, will re-discover: %s", ex)
102                self._home_map_content = {}
103            else:
104                return
105
106        if self._status_trait.state == RoborockStateCode.cleaning:
107            raise RoborockDeviceBusy("Cannot perform home discovery while the device is cleaning")
108
109        await self._maps_trait.refresh()
110        if self._maps_trait.current_map_info is None:
111            raise RoborockException("Cannot perform home discovery without current map info")
112
113        home_map_info, home_map_content = await self._build_home_map_info()
114        _LOGGER.debug("Home discovery complete, caching data for %d maps", len(home_map_info))
115        self._discovery_completed = True
116        await self._update_home_cache(home_map_info, home_map_content)
117
118    async def _refresh_map_info(self, map_info) -> CombinedMapInfo:
119        """Collect room data for a specific map and return CombinedMapInfo."""
120        await self._rooms_trait.refresh()
121
122        rooms: dict[int, NamedRoomMapping] = {}
123        if map_info.rooms:
124            # Not all vacuums resopnd with rooms inside map_info.
125            for room in map_info.rooms:
126                if room.id is not None and room.iot_name_id is not None:
127                    rooms[room.id] = NamedRoomMapping(
128                        segment_id=room.id,
129                        iot_id=room.iot_name_id,
130                        name=room.iot_name or "Unknown",
131                    )
132
133        # Add rooms from rooms_trait. If room already exists and rooms_trait has "Unknown", don't override.
134        if self._rooms_trait.rooms:
135            for room in self._rooms_trait.rooms:
136                if room.segment_id is not None and room.name:
137                    if room.segment_id not in rooms or room.name != "Unknown":
138                        # Add the room to rooms if the room segment is not already in it
139                        # or if the room name isn't unknown.
140                        rooms[room.segment_id] = room
141
142        return CombinedMapInfo(
143            map_flag=map_info.map_flag,
144            name=map_info.name,
145            rooms=list(rooms.values()),
146        )
147
148    async def _refresh_map_content(self) -> MapContent:
149        """Refresh the map content trait to get the latest map data."""
150        await self._map_content.refresh()
151        return MapContent(
152            image_content=self._map_content.image_content,
153            map_data=self._map_content.map_data,
154            raw_api_response=self._map_content.raw_api_response,
155        )
156
157    async def _build_home_map_info(self) -> tuple[dict[int, CombinedMapInfo], dict[int, MapContent]]:
158        """Perform the actual discovery and caching of home map info and content."""
159        home_map_info: dict[int, CombinedMapInfo] = {}
160        home_map_content: dict[int, MapContent] = {}
161
162        # Sort map_info to process the current map last, reducing map switching.
163        # False (non-original maps) sorts before True (original map). We ensure
164        # we load the original map last.
165        sorted_map_infos = sorted(
166            self._maps_trait.map_info or [],
167            key=lambda mi: mi.map_flag == self._maps_trait.current_map,
168            reverse=False,
169        )
170        _LOGGER.debug("Building home cache for maps: %s", [mi.map_flag for mi in sorted_map_infos])
171        for map_info in sorted_map_infos:
172            # We need to load each map to get its room data
173            if len(sorted_map_infos) > 1:
174                _LOGGER.debug("Loading map %s", map_info.map_flag)
175                try:
176                    await self._maps_trait.set_current_map(map_info.map_flag)
177                except RoborockInvalidStatus as ex:
178                    # Device is in a state that forbids map switching. Translate to
179                    # "busy" so callers can fall back to refreshing the current map only.
180                    raise RoborockDeviceBusy("Cannot switch maps right now (device action locked)") from ex
181                await asyncio.sleep(MAP_SLEEP)
182
183            map_content = await self._refresh_map_content()
184            home_map_content[map_info.map_flag] = map_content
185
186            combined_map_info = await self._refresh_map_info(map_info)
187            home_map_info[map_info.map_flag] = combined_map_info
188        return home_map_info, home_map_content
189
190    async def refresh(self) -> None:
191        """Refresh current map's underlying map and room data, updating cache as needed.
192
193        This will only refresh the current map's data and will not populate non
194        active maps or re-discover the home. It is expected that this will keep
195        information up to date for the current map as users switch to that map.
196        """
197        if not self._discovery_completed:
198            # Running initial discovery also populates all of the same information
199            # as below so we can just call that method. If the device is busy
200            # then we'll fall through below to refresh the current map only.
201            try:
202                await self.discover_home()
203                return
204            except RoborockDeviceBusy:
205                _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
206
207        # Refresh the list of map names/info
208        await self._maps_trait.refresh()
209        if (current_map_info := self._maps_trait.current_map_info) is None or (
210            map_flag := self._maps_trait.current_map
211        ) is None:
212            raise RoborockException("Cannot refresh home data without current map info")
213
214        # Refresh the map content to ensure we have the latest image and object positions
215        new_map_content = await self._refresh_map_content()
216        # Refresh the current map's room data
217        combined_map_info = await self._refresh_map_info(current_map_info)
218        await self._update_current_map(
219            map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
220        )
221
222    @property
223    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
224        """Returns the map information for all cached maps."""
225        return self._home_map_info
226
227    @property
228    def current_map_data(self) -> CombinedMapInfo | None:
229        """Returns the map data for the current map."""
230        current_map_flag = self._maps_trait.current_map
231        if current_map_flag is None or self._home_map_info is None:
232            return None
233        return self._home_map_info.get(current_map_flag)
234
235    @property
236    def home_map_content(self) -> dict[int, MapContent] | None:
237        """Returns the map content for all cached maps."""
238        return self._home_map_content
239
240    def _parse_response(self, response: common.V1ResponseData) -> Self:
241        """This trait does not parse responses directly."""
242        raise NotImplementedError("HomeTrait does not support direct command responses")
243
244    async def _update_home_cache(
245        self, home_map_info: dict[int, CombinedMapInfo], home_map_content: dict[int, MapContent]
246    ) -> None:
247        """Update the entire home cache with new map info and content."""
248        device_cache_data = await self._device_cache.get()
249        device_cache_data.home_map_info = home_map_info
250        device_cache_data.home_map_content_base64 = {
251            k: base64.b64encode(v.raw_api_response).decode("utf-8")
252            for k, v in home_map_content.items()
253            if v.raw_api_response
254        }
255        await self._device_cache.set(device_cache_data)
256        self._home_map_info = home_map_info
257        self._home_map_content = home_map_content
258
259    async def _update_current_map(
260        self,
261        map_flag: int,
262        map_info: CombinedMapInfo,
263        map_content: MapContent,
264        update_cache: bool,
265    ) -> None:
266        """Update the cache for the current map only."""
267        # Update the persistent cache if requested e.g. home discovery has
268        # completed and we want to keep it fresh. Otherwise just update the
269        # in memory map below.
270        if update_cache:
271            device_cache_data = await self._device_cache.get()
272            if device_cache_data.home_map_info is None:
273                device_cache_data.home_map_info = {}
274            device_cache_data.home_map_info[map_flag] = map_info
275            if map_content.raw_api_response:
276                if device_cache_data.home_map_content_base64 is None:
277                    device_cache_data.home_map_content_base64 = {}
278                device_cache_data.home_map_content_base64[map_flag] = base64.b64encode(
279                    map_content.raw_api_response
280                ).decode("utf-8")
281            await self._device_cache.set(device_cache_data)
282
283        if self._home_map_info is None:
284            self._home_map_info = {}
285        self._home_map_info[map_flag] = map_info
286
287        if self._home_map_content is None:
288            self._home_map_content = {}
289        self._home_map_content[map_flag] = map_content

Trait that represents a full view of the home layout.

HomeTrait( status_trait: roborock.devices.traits.v1.status.StatusTrait, maps_trait: <function mqtt_rpc_channel.<locals>.wrapper>, map_content: <function map_rpc_channel.<locals>.wrapper>, rooms_trait: roborock.devices.traits.v1.rooms.RoomsTrait, device_cache: roborock.devices.cache.DeviceCache)
46    def __init__(
47        self,
48        status_trait: StatusTrait,
49        maps_trait: MapsTrait,
50        map_content: MapContentTrait,
51        rooms_trait: RoomsTrait,
52        device_cache: DeviceCache,
53    ) -> None:
54        """Initialize the HomeTrait.
55
56        We keep track of the MapsTrait and RoomsTrait to provide a comprehensive
57        view of the home layout. This also depends on the StatusTrait to determine
58        the current map. See comments in MapsTrait for details on that dependency.
59
60        The cache is used to store discovered home data to minimize map switching
61        and improve performance. The cache should be persisted by the caller to
62        ensure data is retained across restarts.
63
64        After initial discovery, only information for the current map is refreshed
65        to keep data up to date without excessive map switching. However, as
66        users switch rooms, the current map's data will be updated to ensure
67        accuracy.
68        """
69        super().__init__()
70        self._status_trait = status_trait
71        self._maps_trait = maps_trait
72        self._map_content = map_content
73        self._rooms_trait = rooms_trait
74        self._device_cache = device_cache
75        self._discovery_completed = False
76        self._home_map_info: dict[int, CombinedMapInfo] | None = None
77        self._home_map_content: dict[int, MapContent] | None = None

Initialize the HomeTrait.

We keep track of the MapsTrait and RoomsTrait to provide a comprehensive view of the home layout. This also depends on the StatusTrait to determine the current map. See comments in MapsTrait for details on that dependency.

The cache is used to store discovered home data to minimize map switching and improve performance. The cache should be persisted by the caller to ensure data is retained across restarts.

After initial discovery, only information for the current map is refreshed to keep data up to date without excessive map switching. However, as users switch rooms, the current map's data will be updated to ensure accuracy.

command = <RoborockCommand.GET_MAP_V1: 'get_map_v1'>
async def discover_home(self) -> None:
 79    async def discover_home(self) -> None:
 80        """Iterate through all maps to discover rooms and cache them.
 81
 82        This will be a no-op if the home cache is already populated.
 83
 84        This cannot be called while the device is cleaning, as that would interrupt the
 85        cleaning process. This will raise `RoborockDeviceBusy` if the device is
 86        currently cleaning.
 87
 88        After discovery, the home cache will be populated and can be accessed via the `home_map_info` property.
 89        """
 90        device_cache_data = await self._device_cache.get()
 91        if device_cache_data and device_cache_data.home_map_info:
 92            _LOGGER.debug("Home cache already populated, skipping discovery")
 93            self._home_map_info = device_cache_data.home_map_info
 94            self._discovery_completed = True
 95            try:
 96                self._home_map_content = {
 97                    k: self._map_content.parse_map_content(base64.b64decode(v))
 98                    for k, v in (device_cache_data.home_map_content_base64 or {}).items()
 99                }
100            except (ValueError, RoborockException) as ex:
101                _LOGGER.warning("Failed to parse cached home map content, will re-discover: %s", ex)
102                self._home_map_content = {}
103            else:
104                return
105
106        if self._status_trait.state == RoborockStateCode.cleaning:
107            raise RoborockDeviceBusy("Cannot perform home discovery while the device is cleaning")
108
109        await self._maps_trait.refresh()
110        if self._maps_trait.current_map_info is None:
111            raise RoborockException("Cannot perform home discovery without current map info")
112
113        home_map_info, home_map_content = await self._build_home_map_info()
114        _LOGGER.debug("Home discovery complete, caching data for %d maps", len(home_map_info))
115        self._discovery_completed = True
116        await self._update_home_cache(home_map_info, home_map_content)

Iterate through all maps to discover rooms and cache them.

This will be a no-op if the home cache is already populated.

This cannot be called while the device is cleaning, as that would interrupt the cleaning process. This will raise RoborockDeviceBusy if the device is currently cleaning.

After discovery, the home cache will be populated and can be accessed via the home_map_info property.

async def refresh(self) -> None:
190    async def refresh(self) -> None:
191        """Refresh current map's underlying map and room data, updating cache as needed.
192
193        This will only refresh the current map's data and will not populate non
194        active maps or re-discover the home. It is expected that this will keep
195        information up to date for the current map as users switch to that map.
196        """
197        if not self._discovery_completed:
198            # Running initial discovery also populates all of the same information
199            # as below so we can just call that method. If the device is busy
200            # then we'll fall through below to refresh the current map only.
201            try:
202                await self.discover_home()
203                return
204            except RoborockDeviceBusy:
205                _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
206
207        # Refresh the list of map names/info
208        await self._maps_trait.refresh()
209        if (current_map_info := self._maps_trait.current_map_info) is None or (
210            map_flag := self._maps_trait.current_map
211        ) is None:
212            raise RoborockException("Cannot refresh home data without current map info")
213
214        # Refresh the map content to ensure we have the latest image and object positions
215        new_map_content = await self._refresh_map_content()
216        # Refresh the current map's room data
217        combined_map_info = await self._refresh_map_info(current_map_info)
218        await self._update_current_map(
219            map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
220        )

Refresh current map's underlying map and room data, updating cache as needed.

This will only refresh the current map's data and will not populate non active maps or re-discover the home. It is expected that this will keep information up to date for the current map as users switch to that map.

home_map_info: dict[int, roborock.data.containers.CombinedMapInfo] | None
222    @property
223    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
224        """Returns the map information for all cached maps."""
225        return self._home_map_info

Returns the map information for all cached maps.

current_map_data: roborock.data.containers.CombinedMapInfo | None
227    @property
228    def current_map_data(self) -> CombinedMapInfo | None:
229        """Returns the map data for the current map."""
230        current_map_flag = self._maps_trait.current_map
231        if current_map_flag is None or self._home_map_info is None:
232            return None
233        return self._home_map_info.get(current_map_flag)

Returns the map data for the current map.

home_map_content: dict[int, roborock.devices.traits.v1.map_content.MapContent] | None
235    @property
236    def home_map_content(self) -> dict[int, MapContent] | None:
237        """Returns the map content for all cached maps."""
238        return self._home_map_content

Returns the map content for all cached maps.