Skip to content

feat: use get_rooms to limit issues with missing room names#780

Merged
Lash-L merged 8 commits intomainfrom
room_name_improvements
Mar 8, 2026
Merged

feat: use get_rooms to limit issues with missing room names#780
Lash-L merged 8 commits intomainfrom
room_name_improvements

Conversation

@Lash-L
Copy link
Collaborator

@Lash-L Lash-L commented Mar 7, 2026

Another one I don't know for sure if this was the root of the problem but is hopefully enough to fix the problem.

The app seems to rely a lot more on the get_rooms endpoint than home data rooms, it's possible some vacs are using it very limitedly. So what we do is this:

If we get a room back with an iot id we have not seen before/ is unknown, we call get_rooms(), try to connect it, and update the home_info as well.

If the room is unknown but we have already seen it, then we do not call get_rooms to not overwhelm the web api.

Relates to: home-assistant/core#165006

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to reduce cases where room names are missing/Unknown by having HomeTrait._refresh_map_info() optionally query the cloud get_rooms() endpoint when it encounters previously-unseen unknown room IOT IDs, then merge any newly discovered rooms back into home data and avoid repeated calls for already-seen unknown IDs.

Changes:

  • Add optional UserWebApiClient dependency to HomeTrait and track seen unknown room IOT IDs per map to conditionally call get_rooms().
  • Fix/adjust the web API get_rooms() request path and expose a get_rooms() helper on UserWebApiClient.
  • Add/expand tests to validate get_rooms() resolution behavior and call-throttling.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
roborock/devices/traits/v1/home.py Adds conditional get_rooms() lookup + seen-IOT tracking; seeds seen IDs from cache/current map.
roborock/devices/traits/v1/rooms.py Adds helper to merge web rooms into HomeData.rooms.
roborock/web_api.py Fixes get_rooms() request path/auth string; adds UserWebApiClient.get_rooms().
roborock/devices/traits/v1/__init__.py Wires web_api into HomeTrait construction for v1 devices.
tests/devices/traits/v1/test_home.py Adds fixtures and multiple tests around web get_rooms() resolution and throttling behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

self._rooms_trait.merge_home_data_rooms(web_rooms)
except Exception:
# Broad exception as we don't want anything here to make us fail upwards, we are okay with 'unknowns'
_LOGGER.debug("Failed to fetch rooms from web API for map %s", map_info.map_flag)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching a broad Exception here logs only a generic debug message, which makes diagnosing web API failures difficult (and the exception is silently dropped). Consider logging the exception details as well (e.g., include the exception object or use exc_info=True) while still keeping the failure non-fatal.

Suggested change
_LOGGER.debug("Failed to fetch rooms from web API for map %s", map_info.map_flag)
_LOGGER.debug(
"Failed to fetch rooms from web API for map %s",
map_info.map_flag,
exc_info=True,
)

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +65
"""Merge newly discovered rooms into home data by room id."""
existing_ids = {room.id for room in self._home_data.rooms or ()}
updated_rooms = list(self._home_data.rooms or ())
for room in rooms:
if room.id not in existing_ids:
updated_rooms.append(room)
existing_ids.add(room.id)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge_home_data_rooms() only appends rooms with new IDs; it never updates names for existing room IDs. If the goal is to resolve missing/incorrect room names via get_rooms(), you likely want to also update existing entries when the incoming room name differs (or when the existing name is "Unknown"), otherwise future refreshes may keep returning stale names.

Suggested change
"""Merge newly discovered rooms into home data by room id."""
existing_ids = {room.id for room in self._home_data.rooms or ()}
updated_rooms = list(self._home_data.rooms or ())
for room in rooms:
if room.id not in existing_ids:
updated_rooms.append(room)
existing_ids.add(room.id)
"""Merge newly discovered rooms into home data by room id.
- Rooms with new IDs are appended.
- Existing rooms have their names updated when a better name is provided.
"""
updated_rooms = list(self._home_data.rooms or ())
# Map existing rooms by ID for quick lookup and in-place updates.
existing_by_id: dict[int, HomeDataRoom] = {room.id: room for room in updated_rooms}
for room in rooms:
existing = existing_by_id.get(room.id)
if existing is None:
# New room ID: append and track it.
updated_rooms.append(room)
existing_by_id[room.id] = room
continue
# Existing room: update name if the incoming one is better / more precise.
incoming_name = (getattr(room, "name", None) or "").strip()
if not incoming_name:
# Nothing useful to update with.
continue
current_name = getattr(existing, "name", None)
if current_name is None or current_name == "" or current_name == _DEFAULT_NAME or current_name != incoming_name:
existing.name = incoming_name

Copilot uses AI. Check for mistakes.
assert map_123_data.rooms[0].name == "Example room 3"
assert map_123_data.rooms[1].segment_id == 19
assert map_123_data.rooms[1].name == "Unknown" # Not in mock home data
assert map_123_data.rooms[1].name == "Map 123" # Not in mock home data
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects unknown room names to fall back to "Map 123", but the implementation currently falls back to Room {segment_id} for unresolved names. Update the expected value (or change the fallback behavior) so the test matches the code path.

Suggested change
assert map_123_data.rooms[1].name == "Map 123" # Not in mock home data
assert map_123_data.rooms[1].name == "Room 19" # Not in mock home data; fallback name

Copilot uses AI. Check for mistakes.
Comment on lines 692 to 695
# Room 17: from rooms_trait with "Unknown", falls back to "Map 0"
assert sorted_rooms[1].segment_id == 17
assert sorted_rooms[1].name == "Unknown"
assert sorted_rooms[1].name == "Map 0"
assert sorted_rooms[1].iot_id == "2362044"
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions expect unknown rooms to fall back to "Map 0", but the current implementation replaces unresolved "Unknown" names with Room {segment_id}. Adjust the test expectations (or the fallback implementation) so they are consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +768 to +770
# Both calls should fall back to "Map 42"
assert result1.rooms[0].name == "Map 42"
assert result2.rooms[0].name == "Map 42"
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects a "Map 42" fallback for unresolved room names, but the implementation currently falls back to Room {segment_id}. As written, this test will fail unless the fallback behavior is changed.

Suggested change
# Both calls should fall back to "Map 42"
assert result1.rooms[0].name == "Map 42"
assert result2.rooms[0].name == "Map 42"
# Both calls should fall back to "Room 16"
assert result1.rooms[0].name == "Room 16"
assert result2.rooms[0].name == "Room 16"

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +175
# Replace remaining "Unknown" names with "Room {room_id}" fallback
for segment_id, room in rooms.items():
if room.name == "Unknown":
rooms[segment_id] = NamedRoomMapping(
segment_id=room.segment_id,
iot_id=room.iot_id,
name=f"Room {room.segment_id}",
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback for unresolved room names replaces "Unknown" with Room {segment_id} (see this block), but the updated tests in this PR expect a Map {map_flag} fallback (e.g., "Map 123", "Map 0"). As-is, this will cause the new/updated tests to fail; please align either the implementation or the test expectations to a single fallback format.

Suggested change
# Replace remaining "Unknown" names with "Room {room_id}" fallback
for segment_id, room in rooms.items():
if room.name == "Unknown":
rooms[segment_id] = NamedRoomMapping(
segment_id=room.segment_id,
iot_id=room.iot_id,
name=f"Room {room.segment_id}",
# Replace remaining "Unknown" names with "Map {map_flag}" fallback
for segment_id, room in rooms.items():
if room.name == "Unknown":
rooms[segment_id] = NamedRoomMapping(
segment_id=room.segment_id,
iot_id=room.iot_id,
name=f"Map {map_info.map_flag}",

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

❌ Patch coverage is 92.50000% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
roborock/devices/traits/v1/rooms.py 84.84% 3 Missing and 2 partials ⚠️
roborock/web_api.py 33.33% 2 Missing ⚠️
tests/devices/traits/v1/test_home.py 83.33% 2 Missing ⚠️
Files with missing lines Coverage Δ
roborock/devices/traits/v1/__init__.py 90.50% <100.00%> (ø)
roborock/devices/traits/v1/home.py 91.85% <100.00%> (+0.06%) ⬆️
tests/devices/traits/v1/fixtures.py 98.07% <100.00%> (+0.03%) ⬆️
tests/devices/traits/v1/test_rooms.py 98.03% <100.00%> (+3.92%) ⬆️
roborock/web_api.py 52.60% <33.33%> (-0.02%) ⬇️
tests/devices/traits/v1/test_home.py 98.57% <83.33%> (-0.70%) ⬇️
roborock/devices/traits/v1/rooms.py 77.27% <84.84%> (+3.46%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@allenporter allenporter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be handled through RoomsTrait since home data depends on it already?

@allenporter
Copy link
Contributor

can this be handled through RoomsTrait since home data depends on it already?

Yeah after looking closer this semes like its trying to fix problems with a broken NamedRoomMapping so i think it should be the responsibility of the rooms trait to fix.

@Lash-L Lash-L requested a review from allenporter March 7, 2026 18:34
@Lash-L
Copy link
Collaborator Author

Lash-L commented Mar 7, 2026

Yeah after looking closer this semes like its trying to fix problems with a broken NamedRoomMapping so i think it should be the responsibility of the rooms trait to fix.

Yep great point! Agreed!

# or if the room name isn't unknown.
rooms[room.segment_id] = room

await self._rooms_trait.resolve_unknown_room_names(rooms)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be handled by _rooms_trait.refresh()? It can notice that it sees rooms that are not found and decide what to do about it directly, rather than later trying to reverse engineer what rooms were unseen that need to be re-resolved.

I would just fix it up front and change how _iot_id_room_name_map works so that we check for unknown rooms, then populate the missing rooms in the home data, then create the named room map once based on what rooms we have.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah moved things around. I kept some in home, as we need to do the room renaming of unknowns there. (i.e. Room 3 instead of unknown)

@Lash-L Lash-L requested a review from allenporter March 7, 2026 20:29
rooms[room.segment_id] = room

for segment_id, room in rooms.items():
if room.name == "Unknown":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can RoomsTrait also handle this? It seems like it has enough information to be able to create the rooms with the correct names from the start.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this?

Suggested change
NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, name=name_map.get(iot_id, f"Room {segment_id}"))

if not isinstance(response, list):
raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")

segment_pairs = _extract_segment_pairs(response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK now that its all in one file, there is a lot of back and forth between these methods, and mutating of state, multiple checks for the default name, and a few of these methods and properties called a few different times.

I think he flow is:

  • get the list of present iot ids in the response (and segments)
  • determine if any are missing in the room map
  • fetch the new room map
  • then we merge the room maps (can we just overwrite given we just got the latest rooms rather than merge?)
  • build the namedRoomMapping (again extracting the present iot ids and segments in the response

I think what would simplify things is to make _extract_segment_pairs return a dict instead of a tuple, then this function can do something like this:

segment_map = _extract_segment_map(response)    # Get list of present iot ids  
if (set(segment_map.values()) - set(_iot_id_room_name_map.keys()):  # Check missing
    self._home_data.rooms = await self._refresh_rooms()  # Overwrite, updates _iot_id_room_name_map
new_data = self._parse_response(response, segment_map)   # Build new map
self._update_trait_values(new_data)

Would a flow like that work? I think it would have all the side effects visible here. (This sill has a hidden interaction with _iot_id_room_name_map inside parse_response but maybe ok)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense from my understanding of the apis now.

I keep a 'Unknown' check even though i don't think it will hit as I'm not 100% sure how the cache will handle things.

Check it out let me know your thoughts, appreciate you being thorough on this, hopefully we can get it right and stop having this bug.

allenporter
allenporter previously approved these changes Mar 8, 2026
Copy link
Contributor

@allenporter allenporter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm still not sure why home trait needs to change at all since its just hacking on room trait values? I don't think this really addressed the feedback about code structure at all in terms of not separating home traits vs room trait responsibilities and avoiding side effects within functions of room traits. Feel free to look closer later at the feedback i guess?

@Lash-L Lash-L marked this pull request as draft March 8, 2026 03:52
@Lash-L
Copy link
Collaborator Author

Lash-L commented Mar 8, 2026

i'm still not sure why home trait needs to change at all since its just hacking on room trait values? I don't think this really addressed the feedback about code structure at all in terms of not separating home traits vs room trait responsibilities and avoiding side effects within functions of room traits. Feel free to look closer later at the feedback i guess?

Marking as a draft for now and will take another look in the morning. I may have been misunderstanding things so I'll give everything a good read. But HomeTrait's change is pretty simple, it's mainly being changed because we have two different 'unknown'/fallback map names now. Unknown(I think this should only be from cache now) or Room {#} and we need to check both.

But for the larger code structure comments, I'll take a bigger look and dive into it some more tomorrow. I don't think it's worth merging in right now if you think there are still some structural issues, so I'm going to hold off until I get a better grasp of your comments.

@allenporter
Copy link
Contributor

i'm still not sure why home trait needs to change at all since its just hacking on room trait values? I don't think this really addressed the feedback about code structure at all in terms of not separating home traits vs room trait responsibilities and avoiding side effects within functions of room traits. Feel free to look closer later at the feedback i guess?

Marking as a draft for now and will take another look in the morning. I may have been misunderstanding things so I'll give everything a good read. But HomeTrait's change is pretty simple, it's mainly being changed because we have two different 'unknown'/fallback map names now. Unknown(I think this should only be from cache now) or Room {#} and we need to check both.

Can it just always use the room names now that they always have a name, even if placeholder?

But for the larger code structure comments, I'll take a bigger look and dive into it some more tomorrow. I don't think it's worth merging in right now if you think there are still some structural issues, so I'm going to hold off until I get a better grasp of your comments.

HomeDataRoom(id=9999802, name="Kitchen"),
]

result = await home_trait._refresh_map_info(map_info)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either move to room trait tests or use public home apis?

@Lash-L Lash-L marked this pull request as ready for review March 8, 2026 19:25
@Lash-L Lash-L requested a review from allenporter March 8, 2026 19:25
@Lash-L Lash-L merged commit 2bd569c into main Mar 8, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants