From 1d26979f8ac46bbb0a85e6c4d52246bbc13bfb69 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Thu, 30 Jul 2026 10:22:06 +0200 Subject: [PATCH] Add map entities when home data arrives, not only at setup Both platforms built their entities once, in async_setup_entry, and skipped any coordinator whose properties_api.home was still None. That is a race, not an invariant. The core Roborock coordinator calls discover_home() during setup, and a device that happens to be busy at that moment raises RoborockDeviceBusy, which the coordinator swallows with an _LOGGER.info. Home data lands a little later via the normal update cycle - but by then async_setup_entry has already returned, so that device silently gets no map image and no rotation select. There is no log line and no retry; the only cure is reloading the config entry by hand, and on a two-robot setup one of them can lose the race on most boots. Keep a set of the (device, map_flag) pairs already added and re-run the scan on every coordinator update, so entities appear as soon as their home data does. As a side effect this also picks up maps created in the Roborock app while Home Assistant is running, which previously needed a reload too. The listeners are registered through config_entry.async_on_unload so they are torn down with the entry. --- .../roborock_custom_map/image.py | 54 ++++++++++++++----- .../roborock_custom_map/select.py | 49 ++++++++++++----- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/custom_components/roborock_custom_map/image.py b/custom_components/roborock_custom_map/image.py index a43b917..8d937eb 100644 --- a/custom_components/roborock_custom_map/image.py +++ b/custom_components/roborock_custom_map/image.py @@ -74,19 +74,47 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Roborock image platform.""" - async_add_entities( - RoborockMap( - config_entry, - f"{coord.duid_slug}_custom_map_{map_info.name or f'Map {map_info.map_flag}'}", - coord, - coord.properties_api.home, - map_info.map_flag, - map_info.name, - ) - for coord in config_entry.runtime_data - if coord.properties_api.home is not None - for map_info in (coord.properties_api.home.home_map_info or {}).values() - ) + added: set[tuple[str, int]] = set() + + @callback + def _async_add_new_maps() -> None: + """Add an entity for every map that has appeared since the last check. + + Home data is not guaranteed to be populated by the time this platform + is set up: a device that is busy when Home Assistant starts makes the + Roborock coordinator skip `discover_home()`, so `home_map_info` is + still empty here and that device silently ends up with no map entity + until the config entry is reloaded by hand. Re-checking on every + coordinator update picks those maps up as soon as they arrive, and as + a bonus also catches maps created in the Roborock app while Home + Assistant is running. + """ + entities = [] + for coord in config_entry.runtime_data: + if (home := coord.properties_api.home) is None: + continue + for map_info in (home.home_map_info or {}).values(): + key = (coord.duid_slug, map_info.map_flag) + if key in added: + continue + added.add(key) + entities.append( + RoborockMap( + config_entry, + f"{coord.duid_slug}_custom_map_{map_info.name or f'Map {map_info.map_flag}'}", + coord, + home, + map_info.map_flag, + map_info.name, + ) + ) + if entities: + async_add_entities(entities) + + _async_add_new_maps() + + for coord in config_entry.runtime_data: + config_entry.async_on_unload(coord.async_add_listener(_async_add_new_maps)) class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity): diff --git a/custom_components/roborock_custom_map/select.py b/custom_components/roborock_custom_map/select.py index a28d3ba..7878f88 100644 --- a/custom_components/roborock_custom_map/select.py +++ b/custom_components/roborock_custom_map/select.py @@ -7,7 +7,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -29,18 +29,41 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up rotation Select entities (one per map).""" - async_add_entities( - RoborockMapRotationSelect( - config_entry=config_entry, - unique_id=f"{coord.duid_slug}_map_rotation_{map_info.map_flag}", - coordinator=coord, - map_flag=map_info.map_flag, - map_name=map_info.name, - ) - for coord in config_entry.runtime_data - if coord.properties_api.home is not None - for map_info in (coord.properties_api.home.home_map_info or {}).values() - ) + added: set[tuple[str, int]] = set() + + @callback + def _async_add_new_maps() -> None: + """Add a select for every map that has appeared since the last check. + + Mirrors the image platform: home data may still be empty when this + runs, in which case the affected device would get no rotation select + until the config entry is reloaded by hand. See image.py for details. + """ + entities = [] + for coord in config_entry.runtime_data: + if (home := coord.properties_api.home) is None: + continue + for map_info in (home.home_map_info or {}).values(): + key = (coord.duid_slug, map_info.map_flag) + if key in added: + continue + added.add(key) + entities.append( + RoborockMapRotationSelect( + config_entry=config_entry, + unique_id=f"{coord.duid_slug}_map_rotation_{map_info.map_flag}", + coordinator=coord, + map_flag=map_info.map_flag, + map_name=map_info.name, + ) + ) + if entities: + async_add_entities(entities) + + _async_add_new_maps() + + for coord in config_entry.runtime_data: + config_entry.async_on_unload(coord.async_add_listener(_async_add_new_maps)) class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, SelectEntity):