-
Notifications
You must be signed in to change notification settings - Fork 75
feat: use get_rooms to limit issues with missing room names #780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
543ada0
8b80865
000b510
d11a52c
75a6f49
329f545
952d2e2
84f85e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |||||||||||||||||||||||||||||
| from roborock.devices.traits.v1 import common | ||||||||||||||||||||||||||||||
| from roborock.exceptions import RoborockDeviceBusy, RoborockException, RoborockInvalidStatus | ||||||||||||||||||||||||||||||
| from roborock.roborock_typing import RoborockCommand | ||||||||||||||||||||||||||||||
| from roborock.web_api import UserWebApiClient | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from .map_content import MapContent, MapContentTrait | ||||||||||||||||||||||||||||||
| from .maps import MapsTrait | ||||||||||||||||||||||||||||||
|
|
@@ -49,6 +50,7 @@ def __init__( | |||||||||||||||||||||||||||||
| map_content: MapContentTrait, | ||||||||||||||||||||||||||||||
| rooms_trait: RoomsTrait, | ||||||||||||||||||||||||||||||
| device_cache: DeviceCache, | ||||||||||||||||||||||||||||||
| web_api: UserWebApiClient | None = None, | ||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||
| """Initialize the HomeTrait. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -71,6 +73,8 @@ def __init__( | |||||||||||||||||||||||||||||
| self._map_content = map_content | ||||||||||||||||||||||||||||||
| self._rooms_trait = rooms_trait | ||||||||||||||||||||||||||||||
| self._device_cache = device_cache | ||||||||||||||||||||||||||||||
| self._web_api = web_api | ||||||||||||||||||||||||||||||
| self._seen_room_iot_ids_by_map: dict[int, set[str]] = {} | ||||||||||||||||||||||||||||||
| self._discovery_completed = False | ||||||||||||||||||||||||||||||
| self._home_map_info: dict[int, CombinedMapInfo] | None = None | ||||||||||||||||||||||||||||||
| self._home_map_content: dict[int, MapContent] | None = None | ||||||||||||||||||||||||||||||
|
|
@@ -90,6 +94,7 @@ async def discover_home(self) -> None: | |||||||||||||||||||||||||||||
| if device_cache_data and device_cache_data.home_map_info: | ||||||||||||||||||||||||||||||
| _LOGGER.debug("Home cache already populated, skipping discovery") | ||||||||||||||||||||||||||||||
| self._home_map_info = device_cache_data.home_map_info | ||||||||||||||||||||||||||||||
| self._seed_seen_room_iot_ids(device_cache_data.home_map_info) | ||||||||||||||||||||||||||||||
| self._discovery_completed = True | ||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| self._home_map_content = { | ||||||||||||||||||||||||||||||
|
|
@@ -138,6 +143,40 @@ async def _refresh_map_info(self, map_info) -> CombinedMapInfo: | |||||||||||||||||||||||||||||
| # or if the room name isn't unknown. | ||||||||||||||||||||||||||||||
| rooms[room.segment_id] = room | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # If we discovered at least one new unknown room iot_id for this map, fetch names from web API. | ||||||||||||||||||||||||||||||
| seen_room_iot_ids = self._seen_room_iot_ids_by_map.setdefault(map_info.map_flag, set()) | ||||||||||||||||||||||||||||||
| has_new_unknown_room_iot_id = any( | ||||||||||||||||||||||||||||||
| room.name == "Unknown" and room.iot_id not in seen_room_iot_ids for room in rooms.values() | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| if self._web_api and has_new_unknown_room_iot_id: | ||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| web_rooms = await self._web_api.get_rooms() | ||||||||||||||||||||||||||||||
| web_room_names = {str(r.id): r.name for r in web_rooms} | ||||||||||||||||||||||||||||||
| for segment_id, room in rooms.items(): | ||||||||||||||||||||||||||||||
| if room.name == "Unknown" and room.iot_id in web_room_names: | ||||||||||||||||||||||||||||||
| rooms[segment_id] = NamedRoomMapping( | ||||||||||||||||||||||||||||||
| segment_id=room.segment_id, | ||||||||||||||||||||||||||||||
| iot_id=room.iot_id, | ||||||||||||||||||||||||||||||
| name=web_room_names[room.iot_id], | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| # Merge new rooms into home_data so future refreshes benefit | ||||||||||||||||||||||||||||||
| if web_rooms: | ||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Replace remaining "Unknown" names with "Room {room_id}" fallback | ||||||||||||||||||||||||||||||
| for segment_id, room in rooms.items(): | ||||||||||||||||||||||||||||||
| if room.name == "Unknown": | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||||||||||||
| 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 "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}", |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from roborock.data import HomeData, NamedRoomMapping, RoborockBase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from roborock.data import HomeData, HomeDataRoom, NamedRoomMapping, RoborockBase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from roborock.devices.traits.v1 import common | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from roborock.roborock_typing import RoborockCommand | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -55,6 +55,16 @@ def _parse_response(self, response: common.V1ResponseData) -> Rooms: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def merge_home_data_rooms(self, rooms: list[HomeDataRoom]) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """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.""" | |
| 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 |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |||||||||||||
|
|
||||||||||||||
| import pytest | ||||||||||||||
|
|
||||||||||||||
| from roborock.data.containers import CombinedMapInfo, NamedRoomMapping | ||||||||||||||
| from roborock.data.containers import CombinedMapInfo, HomeDataRoom, NamedRoomMapping | ||||||||||||||
| from roborock.data.v1.v1_code_mappings import RoborockStateCode | ||||||||||||||
| from roborock.data.v1.v1_containers import MultiMapsListMapInfo, MultiMapsListRoom | ||||||||||||||
| from roborock.devices.cache import DeviceCache, DeviceCacheData, InMemoryCache | ||||||||||||||
|
|
@@ -139,16 +139,25 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait: | |||||||||||||
| return device.v1_properties.rooms | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @pytest.fixture | ||||||||||||||
| def mock_web_api() -> AsyncMock: | ||||||||||||||
| """Create a mock web API client.""" | ||||||||||||||
| mock = AsyncMock() | ||||||||||||||
| mock.get_rooms.return_value = [] | ||||||||||||||
| return mock | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @pytest.fixture | ||||||||||||||
| def home_trait( | ||||||||||||||
| status_trait: StatusTrait, | ||||||||||||||
| maps_trait: MapsTrait, | ||||||||||||||
| map_content_trait: MapContentTrait, | ||||||||||||||
| rooms_trait: RoomsTrait, | ||||||||||||||
| device_cache: DeviceCache, | ||||||||||||||
| mock_web_api: AsyncMock, | ||||||||||||||
| ) -> HomeTrait: | ||||||||||||||
| """Create a HomeTrait instance with mocked dependencies.""" | ||||||||||||||
| return HomeTrait(status_trait, maps_trait, map_content_trait, rooms_trait, device_cache) | ||||||||||||||
| return HomeTrait(status_trait, maps_trait, map_content_trait, rooms_trait, device_cache, mock_web_api) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @pytest.fixture(autouse=True) | ||||||||||||||
|
|
@@ -227,7 +236,7 @@ async def test_discover_home_empty_cache( | |||||||||||||
| assert map_123_data.rooms[0].segment_id == 18 | ||||||||||||||
| 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 | ||||||||||||||
|
||||||||||||||
| 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
AI
Mar 7, 2026
There was a problem hiding this comment.
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
AI
Mar 7, 2026
There was a problem hiding this comment.
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.
| # 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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching a broad
Exceptionhere 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 useexc_info=True) while still keeping the failure non-fatal.