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
 21
 22from roborock.data import CombinedMapInfo, MultiMapsListMapInfo, NamedRoomMapping, RoborockBase
 23from roborock.data.v1.v1_code_mappings import RoborockStateCode
 24from roborock.devices.cache import DeviceCache
 25from roborock.devices.traits.v1 import common
 26from roborock.exceptions import RoborockDeviceBusy, RoborockException, RoborockInvalidStatus
 27from roborock.roborock_typing import RoborockCommand
 28
 29from .map_content import MapContent, MapContentTrait
 30from .maps import MapsTrait
 31from .rooms import RoomsTrait
 32from .status import StatusTrait
 33
 34_LOGGER = logging.getLogger(__name__)
 35
 36MAP_SLEEP = 3
 37
 38
 39class HomeTrait(RoborockBase, common.V1TraitMixin):
 40    """Trait that represents a full view of the home layout."""
 41
 42    command = RoborockCommand.GET_MAP_V1  # This is not used
 43    converter = common.DefaultConverter(RoborockBase)  # 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.converter.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: MultiMapsListMapInfo) -> CombinedMapInfo:
118        """Collect room data for a specific map and return CombinedMapInfo."""
119        await self._rooms_trait.refresh()
120
121        # We have room names from multiple sources:
122        # - The map_info.rooms which we just received from the MultiMapsList
123        # - RoomsTrait rooms come from the GET_ROOM_MAPPING command for the current device (only)
124        # - RoomsTrait rooms that are pulled from the cloud API
125        # We always prefer the RoomsTrait room names since they are always newer and
126        # just refreshed above.
127        rooms_map: dict[int, NamedRoomMapping] = {
128            **map_info.rooms_map,
129            **{room.segment_id: room for room in self._rooms_trait.rooms or ()},
130        }
131        return CombinedMapInfo(
132            map_flag=map_info.map_flag,
133            name=map_info.name,
134            rooms=list(rooms_map.values()),
135        )
136
137    async def _refresh_map_content(self) -> MapContent:
138        """Refresh the map content trait to get the latest map data."""
139        await self._map_content.refresh()
140        return MapContent(
141            image_content=self._map_content.image_content,
142            map_data=self._map_content.map_data,
143            raw_api_response=self._map_content.raw_api_response,
144        )
145
146    async def _build_home_map_info(self) -> tuple[dict[int, CombinedMapInfo], dict[int, MapContent]]:
147        """Perform the actual discovery and caching of home map info and content."""
148        home_map_info: dict[int, CombinedMapInfo] = {}
149        home_map_content: dict[int, MapContent] = {}
150
151        # Sort map_info to process the current map last, reducing map switching.
152        # False (non-original maps) sorts before True (original map). We ensure
153        # we load the original map last.
154        sorted_map_infos = sorted(
155            self._maps_trait.map_info or [],
156            key=lambda mi: mi.map_flag == self._maps_trait.current_map,
157            reverse=False,
158        )
159        _LOGGER.debug("Building home cache for maps: %s", [mi.map_flag for mi in sorted_map_infos])
160        for map_info in sorted_map_infos:
161            # We need to load each map to get its room data
162            if len(sorted_map_infos) > 1:
163                _LOGGER.debug("Loading map %s", map_info.map_flag)
164                try:
165                    await self._maps_trait.set_current_map(map_info.map_flag)
166                except RoborockInvalidStatus as ex:
167                    # Device is in a state that forbids map switching. Translate to
168                    # "busy" so callers can fall back to refreshing the current map only.
169                    raise RoborockDeviceBusy("Cannot switch maps right now (device action locked)") from ex
170                await asyncio.sleep(MAP_SLEEP)
171
172            map_content = await self._refresh_map_content()
173            home_map_content[map_info.map_flag] = map_content
174
175            combined_map_info = await self._refresh_map_info(map_info)
176            home_map_info[map_info.map_flag] = combined_map_info
177        return home_map_info, home_map_content
178
179    async def refresh(self) -> None:
180        """Refresh current map's underlying map and room data, updating cache as needed.
181
182        This will only refresh the current map's data and will not populate non
183        active maps or re-discover the home. It is expected that this will keep
184        information up to date for the current map as users switch to that map.
185        """
186        if not self._discovery_completed:
187            # Running initial discovery also populates all of the same information
188            # as below so we can just call that method. If the device is busy
189            # then we'll fall through below to refresh the current map only.
190            try:
191                await self.discover_home()
192                return
193            except RoborockDeviceBusy:
194                _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
195
196        # Refresh the list of map names/info
197        await self._maps_trait.refresh()
198        if (current_map_info := self._maps_trait.current_map_info) is None or (
199            map_flag := self._maps_trait.current_map
200        ) is None:
201            raise RoborockException("Cannot refresh home data without current map info")
202
203        # Refresh the map content to ensure we have the latest image and object positions
204        new_map_content = await self._refresh_map_content()
205        # Refresh the current map's room data
206        combined_map_info = await self._refresh_map_info(current_map_info)
207        await self._update_current_map(
208            map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
209        )
210
211    @property
212    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
213        """Returns the map information for all cached maps."""
214        return self._home_map_info
215
216    @property
217    def current_map_data(self) -> CombinedMapInfo | None:
218        """Returns the map data for the current map."""
219        current_map_flag = self._maps_trait.current_map
220        if current_map_flag is None or self._home_map_info is None:
221            return None
222        return self._home_map_info.get(current_map_flag)
223
224    @property
225    def current_rooms(self) -> list[NamedRoomMapping]:
226        """Returns the room names for the current map."""
227        if self.current_map_data is None:
228            return []
229        return self.current_map_data.rooms
230
231    @property
232    def home_map_content(self) -> dict[int, MapContent] | None:
233        """Returns the map content for all cached maps."""
234        return self._home_map_content
235
236    async def _update_home_cache(
237        self, home_map_info: dict[int, CombinedMapInfo], home_map_content: dict[int, MapContent]
238    ) -> None:
239        """Update the entire home cache with new map info and content."""
240        device_cache_data = await self._device_cache.get()
241        device_cache_data.home_map_info = home_map_info
242        device_cache_data.home_map_content_base64 = {
243            k: base64.b64encode(v.raw_api_response).decode("utf-8")
244            for k, v in home_map_content.items()
245            if v.raw_api_response
246        }
247        await self._device_cache.set(device_cache_data)
248        self._home_map_info = home_map_info
249        self._home_map_content = home_map_content
250
251    async def _update_current_map(
252        self,
253        map_flag: int,
254        map_info: CombinedMapInfo,
255        map_content: MapContent,
256        update_cache: bool,
257    ) -> None:
258        """Update the cache for the current map only."""
259        # Update the persistent cache if requested e.g. home discovery has
260        # completed and we want to keep it fresh. Otherwise just update the
261        # in memory map below.
262        if update_cache:
263            device_cache_data = await self._device_cache.get()
264            if device_cache_data.home_map_info is None:
265                device_cache_data.home_map_info = {}
266            device_cache_data.home_map_info[map_flag] = map_info
267            if map_content.raw_api_response:
268                if device_cache_data.home_map_content_base64 is None:
269                    device_cache_data.home_map_content_base64 = {}
270                device_cache_data.home_map_content_base64[map_flag] = base64.b64encode(
271                    map_content.raw_api_response
272                ).decode("utf-8")
273            await self._device_cache.set(device_cache_data)
274
275        if self._home_map_info is None:
276            self._home_map_info = {}
277        self._home_map_info[map_flag] = map_info
278
279        if self._home_map_content is None:
280            self._home_map_content = {}
281        self._home_map_content[map_flag] = map_content
MAP_SLEEP = 3
 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    converter = common.DefaultConverter(RoborockBase)  # 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.converter.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: MultiMapsListMapInfo) -> CombinedMapInfo:
119        """Collect room data for a specific map and return CombinedMapInfo."""
120        await self._rooms_trait.refresh()
121
122        # We have room names from multiple sources:
123        # - The map_info.rooms which we just received from the MultiMapsList
124        # - RoomsTrait rooms come from the GET_ROOM_MAPPING command for the current device (only)
125        # - RoomsTrait rooms that are pulled from the cloud API
126        # We always prefer the RoomsTrait room names since they are always newer and
127        # just refreshed above.
128        rooms_map: dict[int, NamedRoomMapping] = {
129            **map_info.rooms_map,
130            **{room.segment_id: room for room in self._rooms_trait.rooms or ()},
131        }
132        return CombinedMapInfo(
133            map_flag=map_info.map_flag,
134            name=map_info.name,
135            rooms=list(rooms_map.values()),
136        )
137
138    async def _refresh_map_content(self) -> MapContent:
139        """Refresh the map content trait to get the latest map data."""
140        await self._map_content.refresh()
141        return MapContent(
142            image_content=self._map_content.image_content,
143            map_data=self._map_content.map_data,
144            raw_api_response=self._map_content.raw_api_response,
145        )
146
147    async def _build_home_map_info(self) -> tuple[dict[int, CombinedMapInfo], dict[int, MapContent]]:
148        """Perform the actual discovery and caching of home map info and content."""
149        home_map_info: dict[int, CombinedMapInfo] = {}
150        home_map_content: dict[int, MapContent] = {}
151
152        # Sort map_info to process the current map last, reducing map switching.
153        # False (non-original maps) sorts before True (original map). We ensure
154        # we load the original map last.
155        sorted_map_infos = sorted(
156            self._maps_trait.map_info or [],
157            key=lambda mi: mi.map_flag == self._maps_trait.current_map,
158            reverse=False,
159        )
160        _LOGGER.debug("Building home cache for maps: %s", [mi.map_flag for mi in sorted_map_infos])
161        for map_info in sorted_map_infos:
162            # We need to load each map to get its room data
163            if len(sorted_map_infos) > 1:
164                _LOGGER.debug("Loading map %s", map_info.map_flag)
165                try:
166                    await self._maps_trait.set_current_map(map_info.map_flag)
167                except RoborockInvalidStatus as ex:
168                    # Device is in a state that forbids map switching. Translate to
169                    # "busy" so callers can fall back to refreshing the current map only.
170                    raise RoborockDeviceBusy("Cannot switch maps right now (device action locked)") from ex
171                await asyncio.sleep(MAP_SLEEP)
172
173            map_content = await self._refresh_map_content()
174            home_map_content[map_info.map_flag] = map_content
175
176            combined_map_info = await self._refresh_map_info(map_info)
177            home_map_info[map_info.map_flag] = combined_map_info
178        return home_map_info, home_map_content
179
180    async def refresh(self) -> None:
181        """Refresh current map's underlying map and room data, updating cache as needed.
182
183        This will only refresh the current map's data and will not populate non
184        active maps or re-discover the home. It is expected that this will keep
185        information up to date for the current map as users switch to that map.
186        """
187        if not self._discovery_completed:
188            # Running initial discovery also populates all of the same information
189            # as below so we can just call that method. If the device is busy
190            # then we'll fall through below to refresh the current map only.
191            try:
192                await self.discover_home()
193                return
194            except RoborockDeviceBusy:
195                _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
196
197        # Refresh the list of map names/info
198        await self._maps_trait.refresh()
199        if (current_map_info := self._maps_trait.current_map_info) is None or (
200            map_flag := self._maps_trait.current_map
201        ) is None:
202            raise RoborockException("Cannot refresh home data without current map info")
203
204        # Refresh the map content to ensure we have the latest image and object positions
205        new_map_content = await self._refresh_map_content()
206        # Refresh the current map's room data
207        combined_map_info = await self._refresh_map_info(current_map_info)
208        await self._update_current_map(
209            map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
210        )
211
212    @property
213    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
214        """Returns the map information for all cached maps."""
215        return self._home_map_info
216
217    @property
218    def current_map_data(self) -> CombinedMapInfo | None:
219        """Returns the map data for the current map."""
220        current_map_flag = self._maps_trait.current_map
221        if current_map_flag is None or self._home_map_info is None:
222            return None
223        return self._home_map_info.get(current_map_flag)
224
225    @property
226    def current_rooms(self) -> list[NamedRoomMapping]:
227        """Returns the room names for the current map."""
228        if self.current_map_data is None:
229            return []
230        return self.current_map_data.rooms
231
232    @property
233    def home_map_content(self) -> dict[int, MapContent] | None:
234        """Returns the map content for all cached maps."""
235        return self._home_map_content
236
237    async def _update_home_cache(
238        self, home_map_info: dict[int, CombinedMapInfo], home_map_content: dict[int, MapContent]
239    ) -> None:
240        """Update the entire home cache with new map info and content."""
241        device_cache_data = await self._device_cache.get()
242        device_cache_data.home_map_info = home_map_info
243        device_cache_data.home_map_content_base64 = {
244            k: base64.b64encode(v.raw_api_response).decode("utf-8")
245            for k, v in home_map_content.items()
246            if v.raw_api_response
247        }
248        await self._device_cache.set(device_cache_data)
249        self._home_map_info = home_map_info
250        self._home_map_content = home_map_content
251
252    async def _update_current_map(
253        self,
254        map_flag: int,
255        map_info: CombinedMapInfo,
256        map_content: MapContent,
257        update_cache: bool,
258    ) -> None:
259        """Update the cache for the current map only."""
260        # Update the persistent cache if requested e.g. home discovery has
261        # completed and we want to keep it fresh. Otherwise just update the
262        # in memory map below.
263        if update_cache:
264            device_cache_data = await self._device_cache.get()
265            if device_cache_data.home_map_info is None:
266                device_cache_data.home_map_info = {}
267            device_cache_data.home_map_info[map_flag] = map_info
268            if map_content.raw_api_response:
269                if device_cache_data.home_map_content_base64 is None:
270                    device_cache_data.home_map_content_base64 = {}
271                device_cache_data.home_map_content_base64[map_flag] = base64.b64encode(
272                    map_content.raw_api_response
273                ).decode("utf-8")
274            await self._device_cache.set(device_cache_data)
275
276        if self._home_map_info is None:
277            self._home_map_info = {}
278        self._home_map_info[map_flag] = map_info
279
280        if self._home_map_content is None:
281            self._home_map_content = {}
282        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'>

The RoborockCommand used to fetch the trait data from the device (internal only).

converter = DefaultConverter

The converter used to parse the response from the device (internal only).

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

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
212    @property
213    def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
214        """Returns the map information for all cached maps."""
215        return self._home_map_info

Returns the map information for all cached maps.

current_map_data: roborock.data.containers.CombinedMapInfo | None
217    @property
218    def current_map_data(self) -> CombinedMapInfo | None:
219        """Returns the map data for the current map."""
220        current_map_flag = self._maps_trait.current_map
221        if current_map_flag is None or self._home_map_info is None:
222            return None
223        return self._home_map_info.get(current_map_flag)

Returns the map data for the current map.

current_rooms: list[roborock.data.containers.NamedRoomMapping]
225    @property
226    def current_rooms(self) -> list[NamedRoomMapping]:
227        """Returns the room names for the current map."""
228        if self.current_map_data is None:
229            return []
230        return self.current_map_data.rooms

Returns the room names for the current map.

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

Returns the map content for all cached maps.