Repository navigation
fix: recover missing selected maps without caching placeholders - #995
endoftheline818 wants to merge 3 commits into
Conversation
swastiktawate
left a comment
There was a problem hiding this comment.
This is a thorough fix. The recovery path is properly guarded — idle/charging states only, never while cleaning or returning — and the fallback to recover_missing=False on a busy device means a locked action degrades gracefully instead of failing the whole refresh. Filtering placeholder 'NO MAP' images out of both the in-memory and persisted cache is the right call, and the guard matrix plus the exactly-once reload assertion in the tests show the edge cases were thought through.
Lash-L
left a comment
There was a problem hiding this comment.
Automated code review by Gemini 4 Argon in Antigravity using our /review (/code-review) skill (.agents/skills/code-review/SKILL.md / AGENTS.md).
Summary
Well-structured fix for #985 that keeps missing-map recovery and placeholder filtering encapsulated within HomeTrait, avoids redundant LOAD_MULTI_MAP calls during multi-map discovery, and degrades gracefully when map switching is locked (RoborockInvalidStatus -> RoborockDeviceBusy).
- Layer 1 — Public Trait APIs & Consumer Contracts: Preserves the public
HomeTraitAPI (discover_home(),refresh(),home_map_info,home_map_content) while broadeningRoborockDeviceBusyhandling when the vacuum cannot switch maps. - Layer 2 — Data Lifecycle & State Management: Filters empty placeholder maps (
_has_map_image) before updating both in-memory_home_map_contentand persistentDeviceCache, preserving the last known good map across transient empty responses. - Layer 5 — Tests & Fixtures: Comprehensive parameterized coverage in
tests/devices/traits/v1/test_home.pycovering state/flag guards, action-locked fallback, placeholder eviction, and single- vs. multi-map discovery.
A few lifecycle and state-handling items noted inline:
- Include
RoborockStateCode.charger_disconnected(and checkin_fresh_state/is_locating) in_MAP_LOADING_STATES(roborock/devices/traits/v1/home.py#L39): State2(charger_disconnected) is the V1 sleeping / idle-off-dock state when the robot is not on the charger and no task is active (in_fresh_state == 1). Excluding it causesdiscover_home()and missing-map recovery to treat a sleeping/idle off-dock robot as busy. set_current_map()RefreshesStatusTraitBeforeMAP_SLEEP(roborock/devices/traits/v1/home.py#L178-L181):MapsTrait.set_current_map()sendsLOAD_MULTI_MAPand immediately callsself._status_trait.refresh()beforeawait asyncio.sleep(MAP_SLEEP). If the vacuum does not flipmap_presentto1until theMAP_SLEEPdelay completes,self._status_trait.map_presentremains0in memory and can trigger another reload on the nextHomeTrait.refresh()ifStatusTraitis not refreshed again.- Evicting Cached Placeholders in
discover_home()Without Re-Discovering (roborock/devices/traits/v1/home.py#L112-L122): Stripping placeholder entries fromhome_map_content_base64and returning early withself._discovery_completed = Trueleaves non-active maps on multi-map devices permanently unpopulated (sincerefresh()only refreshes the active map once_discovery_completedisTrue). Also, settingself._discovery_completed = Truebefore the cache-parsingtryblock leaks_discovery_completed = Trueif cache parsing fails and the subsequent busy check raisesRoborockDeviceBusy.
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| MAP_SLEEP = 3 | ||
| _MAP_LOADING_STATES = (RoborockStateCode.idle, RoborockStateCode.charging, RoborockStateCode.charging_complete) |
There was a problem hiding this comment.
Include Off-Dock Idle / Sleeping State (RoborockStateCode.charger_disconnected) and Guard Against is_locating
On V1 devices, state 2 (RoborockStateCode.charger_disconnected) represents the sleeping / idle-off-dock state when the vacuum is stationary off the dock with no task running (in_fresh_state == 1, in_cleaning == 0, in_returning == 0). Excluding RoborockStateCode.charger_disconnected from _MAP_LOADING_STATES means a vacuum sitting idle/sleeping off the charger will raise RoborockDeviceBusy during discover_home() and never recover a missing map.
Additionally, in _refresh_map_content() / discover_home(), this should also avoid loading a map when self._status_trait.is_locating == 1 (or when self._status_trait.in_fresh_state is not None and self._status_trait.in_fresh_state != 1), as the robot invalidates the current map ID while locating.
| _MAP_LOADING_STATES = (RoborockStateCode.idle, RoborockStateCode.charging, RoborockStateCode.charging_complete) | |
| _MAP_LOADING_STATES = ( | |
| RoborockStateCode.charger_disconnected, | |
| RoborockStateCode.idle, | |
| RoborockStateCode.charging, | |
| RoborockStateCode.charging_complete, | |
| ) |
There was a problem hiding this comment.
Thanks for reviewing. I’ve left state 2 and the additional status guards unchanged for now. Could you share device diagnostics or protocol references supporting state 2 as safe for map loading and clarify the expected in_fresh_state and is_locating values
| for k, v in (device_cache_data.home_map_content_base64 or {}).items() | ||
| if k in self._home_map_content | ||
| } | ||
| await self._device_cache.set(device_cache_data) |
There was a problem hiding this comment.
Cached Placeholder Cleanup Skips Re-Discovery for Missing Maps & Sets _discovery_completed Too Early
Two subtle lifecycle issues here:
self._discovery_completed = Trueis set on line 102 before thetryblock. Ifasync_parse_map_contentraises(ValueError, RoborockException)and the fallback discovery then raisesRoborockDeviceBusyat line 129,self._discovery_completedremainsTrueeven though discovery failed andself._home_map_contentis{}.- When an older version cached a
"NO MAP"placeholder for a non-active floor on a multi-map device, filtering it out ofself._home_map_contentanddevice_cache_data.home_map_content_base64and returning early withself._discovery_completed = Truemeans that floor's map will never be fetched (since subsequentrefresh()calls only updatecurrent_map_info). Similarly, for a single-map device,discover_home()returns withself._home_map_content == {}until a separaterefresh()is called.
Consider only returning early when all maps in self._home_map_info still have valid map content (or re-running discovery when cached entries had to be evicted), and moving self._discovery_completed = True into the else: branch after cache validation succeeds.
There was a problem hiding this comment.
Addressed in fdc71a8 and ed7b28e. Cached placeholders now trigger rediscovery while good cached images remain available. Discovery stays pending if the robot is busy or no current map is available, and the saved cache remains unchanged until discovery completes. Regression tests cover busy-to-idle recovery and recovery after a current map becomes available.
| await self._maps_trait.set_current_map(current_map_info.map_flag) | ||
| except RoborockInvalidStatus as ex: | ||
| raise RoborockDeviceBusy("Cannot reload selected map right now (device action locked)") from ex | ||
| await asyncio.sleep(MAP_SLEEP) |
There was a problem hiding this comment.
StatusTrait.refresh() Inside set_current_map() Runs Before MAP_SLEEP
self._maps_trait.set_current_map(...) sends LOAD_MULTI_MAP and immediately calls await self._status_trait.refresh() before await asyncio.sleep(MAP_SLEEP) here. As modeled in test_multi_map_discovery_does_not_reload_twice, the immediate GET_STATUS response right after LOAD_MULTI_MAP can still report map_present == 0 while the vacuum is loading the map over MAP_SLEEP.
If a caller runs home_trait.refresh() again without an intervening status_trait.refresh(), self._status_trait.map_present will still be 0 and _refresh_map_content() will issue LOAD_MULTI_MAP + sleep(MAP_SLEEP) again even though the map is now loaded. Refreshing self._status_trait after await asyncio.sleep(MAP_SLEEP) (or skipping recovery when self._home_map_content already holds a valid image for current_map_info.map_flag unless map_present == 0 was freshly observed) avoids redundant reloads.
There was a problem hiding this comment.
Addressed in fdc71a8. Both single-map recovery and multi-map discovery now refresh status again after the map-loading delay. Tests model the immediate status reporting map_present == 0, the later read reporting 1, and a subsequent refresh sending no additional load command.
Discovery used to drop a cached "NO MAP" placeholder, rewrite the cache without it and mark discovery complete, so that floor stayed empty until it became the active map. A placeholder now leaves discovery pending: the good cached maps stay available, the saved cache is not touched, and the home is discovered again on the first idle refresh. A completed discovery replaces the cache, and a floor that answers with a placeholder keeps its last good image. The device cache can hand out the object it stores, so discovery keeps copies in memory instead of changing the cached objects before they are saved. Discovery is marked complete only after it succeeds. MapsTrait.set_current_map() reads the status right after LOAD_MULTI_MAP, which can still report map_present == 0. Read the status again after the load delay in both single-map recovery and multi-map discovery so the next refresh does not send the same load command again.
When a cached placeholder needed rediscovery but the device reported no current map, discovery was marked complete, so the missing floor was never rediscovered once a map became current. Raise RoborockDeviceBusy instead, so refresh() falls back to reading the current map and retries discovery on the next refresh. The saved cache is still left untouched meanwhile. The regression test now continues past the missing current map: a floor becomes current and an ordinary refresh rediscovers the other floor.
|
Updated the PR description with the follow-up fixes and final Linux validation results. I've also replied to all three review threads. Hosted CI for |
Some vacuums show "NO MAP" even though a saved map is already selected. Selecting that same map again restores the image. This change performs that recovery automatically when the vacuum is idle or charging, without interrupting cleaning or a return to the dock.
The fix belongs in HomeTrait, as requested in home-assistant/core#186678:
set_current_map()can still reportmap_present == 0while the map is loading, causing another load command on the next refresh.Fixes #985. Related: home-assistant/core#185133 and home-assistant/core#186678.
Validation:
Tests use simulated device responses. A physical Q5 Pro has not been tested with this change.
AI assistance: OpenAI Codex and Anthropic Claude Code helped prepare, review, and test this change under the contributor's direction.