Skip to content

Commit 000b510

Browse files
committed
chore: address comments
1 parent 8b80865 commit 000b510

5 files changed

Lines changed: 98 additions & 52 deletions

File tree

roborock/devices/traits/v1/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,10 @@ def __init__(
193193
self.device_features = DeviceFeaturesTrait(product, self._device_cache)
194194
self.status = StatusTrait(self.device_features, region=self._region)
195195
self.consumables = ConsumableTrait()
196-
self.rooms = RoomsTrait(home_data)
196+
self.rooms = RoomsTrait(home_data, web_api)
197197
self.maps = MapsTrait(self.status)
198198
self.map_content = MapContentTrait(map_parser_config)
199-
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache, web_api)
199+
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
200200
self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
201201
self.routines = RoutinesTrait(device_uid, web_api)
202202

roborock/devices/traits/v1/home.py

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from roborock.devices.traits.v1 import common
2727
from roborock.exceptions import RoborockDeviceBusy, RoborockException, RoborockInvalidStatus
2828
from roborock.roborock_typing import RoborockCommand
29-
from roborock.web_api import UserWebApiClient
3029

3130
from .map_content import MapContent, MapContentTrait
3231
from .maps import MapsTrait
@@ -50,7 +49,6 @@ def __init__(
5049
map_content: MapContentTrait,
5150
rooms_trait: RoomsTrait,
5251
device_cache: DeviceCache,
53-
web_api: UserWebApiClient | None = None,
5452
) -> None:
5553
"""Initialize the HomeTrait.
5654
@@ -73,8 +71,6 @@ def __init__(
7371
self._map_content = map_content
7472
self._rooms_trait = rooms_trait
7573
self._device_cache = device_cache
76-
self._web_api = web_api
77-
self._seen_room_iot_ids_by_map: dict[int, set[str]] = {}
7874
self._discovery_completed = False
7975
self._home_map_info: dict[int, CombinedMapInfo] | None = None
8076
self._home_map_content: dict[int, MapContent] | None = None
@@ -94,7 +90,6 @@ async def discover_home(self) -> None:
9490
if device_cache_data and device_cache_data.home_map_info:
9591
_LOGGER.debug("Home cache already populated, skipping discovery")
9692
self._home_map_info = device_cache_data.home_map_info
97-
self._seed_seen_room_iot_ids(device_cache_data.home_map_info)
9893
self._discovery_completed = True
9994
try:
10095
self._home_map_content = {
@@ -143,39 +138,7 @@ async def _refresh_map_info(self, map_info) -> CombinedMapInfo:
143138
# or if the room name isn't unknown.
144139
rooms[room.segment_id] = room
145140

146-
# If we discovered at least one new unknown room iot_id for this map, fetch names from web API.
147-
seen_room_iot_ids = self._seen_room_iot_ids_by_map.setdefault(map_info.map_flag, set())
148-
has_new_unknown_room_iot_id = any(
149-
room.name == "Unknown" and room.iot_id not in seen_room_iot_ids for room in rooms.values()
150-
)
151-
if self._web_api and has_new_unknown_room_iot_id:
152-
try:
153-
web_rooms = await self._web_api.get_rooms()
154-
web_room_names = {str(r.id): r.name for r in web_rooms}
155-
for segment_id, room in rooms.items():
156-
if room.name == "Unknown" and room.iot_id in web_room_names:
157-
rooms[segment_id] = NamedRoomMapping(
158-
segment_id=room.segment_id,
159-
iot_id=room.iot_id,
160-
name=web_room_names[room.iot_id],
161-
)
162-
# Merge new rooms into home_data so future refreshes benefit
163-
if web_rooms:
164-
self._rooms_trait.merge_home_data_rooms(web_rooms)
165-
except Exception:
166-
# Broad exception as we don't want anything here to make us fail upwards, we are okay with 'unknowns'
167-
_LOGGER.debug("Failed to fetch rooms from web API for map %s", map_info.map_flag)
168-
169-
# Replace remaining "Unknown" names with "Room {segment_id}" fallback.
170-
for segment_id, room in rooms.items():
171-
if room.name == "Unknown":
172-
rooms[segment_id] = NamedRoomMapping(
173-
segment_id=room.segment_id,
174-
iot_id=room.iot_id,
175-
name=f"Room {room.segment_id}",
176-
)
177-
178-
self._seen_room_iot_ids_by_map[map_info.map_flag].update(room.iot_id for room in rooms.values())
141+
await self._rooms_trait.resolve_unknown_room_names(rooms)
179142

180143
return CombinedMapInfo(
181144
map_flag=map_info.map_flag,
@@ -293,7 +256,6 @@ async def _update_home_cache(
293256
await self._device_cache.set(device_cache_data)
294257
self._home_map_info = home_map_info
295258
self._home_map_content = home_map_content
296-
self._seed_seen_room_iot_ids(home_map_info)
297259

298260
async def _update_current_map(
299261
self,
@@ -322,13 +284,7 @@ async def _update_current_map(
322284
if self._home_map_info is None:
323285
self._home_map_info = {}
324286
self._home_map_info[map_flag] = map_info
325-
self._seed_seen_room_iot_ids({map_flag: map_info})
326287

327288
if self._home_map_content is None:
328289
self._home_map_content = {}
329290
self._home_map_content[map_flag] = map_content
330-
331-
def _seed_seen_room_iot_ids(self, home_map_info: dict[int, CombinedMapInfo]) -> None:
332-
"""Seed known room iot_ids from cached/current map information."""
333-
for map_flag, map_info in home_map_info.items():
334-
self._seen_room_iot_ids_by_map.setdefault(map_flag, set()).update(room.iot_id for room in map_info.rooms)

roborock/devices/traits/v1/rooms.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from roborock.data import HomeData, HomeDataRoom, NamedRoomMapping, RoborockBase
77
from roborock.devices.traits.v1 import common
88
from roborock.roborock_typing import RoborockCommand
9+
from roborock.web_api import UserWebApiClient
910

1011
_LOGGER = logging.getLogger(__name__)
1112

@@ -32,10 +33,12 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
3233

3334
command = RoborockCommand.GET_ROOM_MAPPING
3435

35-
def __init__(self, home_data: HomeData) -> None:
36+
def __init__(self, home_data: HomeData, web_api: UserWebApiClient | None = None) -> None:
3637
"""Initialize the RoomsTrait."""
3738
super().__init__()
3839
self._home_data = home_data
40+
self._web_api = web_api
41+
self._seen_unknown_room_iot_ids: set[str] = set()
3942

4043
@property
4144
def _iot_id_room_name_map(self) -> dict[str, str]:
@@ -57,14 +60,46 @@ def _parse_response(self, response: common.V1ResponseData) -> Rooms:
5760

5861
def merge_home_data_rooms(self, rooms: list[HomeDataRoom]) -> None:
5962
"""Merge newly discovered rooms into home data by room id."""
60-
existing_ids = {room.id for room in self._home_data.rooms or ()}
6163
updated_rooms = list(self._home_data.rooms or ())
64+
existing_by_id = {room.id: room for room in updated_rooms}
65+
6266
for room in rooms:
63-
if room.id not in existing_ids:
67+
existing_room = existing_by_id.get(room.id)
68+
if existing_room is None:
6469
updated_rooms.append(room)
65-
existing_ids.add(room.id)
70+
existing_by_id[room.id] = room
71+
elif room.name and existing_room.name in ("", _DEFAULT_NAME):
72+
existing_room.name = room.name
73+
6674
self._home_data.rooms = updated_rooms
6775

76+
async def resolve_unknown_room_names(self, rooms: dict[int, NamedRoomMapping]) -> None:
77+
"""Resolve unknown room names using home data and web API fallbacks."""
78+
unknown_room_iot_ids = {room.iot_id for room in rooms.values() if room.name == _DEFAULT_NAME}
79+
new_unknown_room_iot_ids = unknown_room_iot_ids - self._seen_unknown_room_iot_ids
80+
web_room_names: dict[str, str] = {}
81+
82+
if self._web_api and new_unknown_room_iot_ids:
83+
try:
84+
web_rooms = await self._web_api.get_rooms()
85+
except Exception:
86+
_LOGGER.debug("Failed to fetch rooms from web API", exc_info=True)
87+
else:
88+
if web_rooms:
89+
web_room_names = {str(room.id): room.name for room in web_rooms}
90+
self.merge_home_data_rooms(web_rooms)
91+
92+
for segment_id, room in rooms.items():
93+
if room.name != _DEFAULT_NAME:
94+
continue
95+
rooms[segment_id] = NamedRoomMapping(
96+
segment_id=room.segment_id,
97+
iot_id=room.iot_id,
98+
name=web_room_names.get(room.iot_id, f"Room {room.segment_id}"),
99+
)
100+
101+
self._seen_unknown_room_iot_ids.update(unknown_room_iot_ids)
102+
68103

69104
def _extract_segment_pairs(response: list) -> list[tuple[int, str]]:
70105
"""Extract segment_id and iot_id pairs from the response.

tests/devices/traits/v1/test_home.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ def home_trait(
157157
mock_web_api: AsyncMock,
158158
) -> HomeTrait:
159159
"""Create a HomeTrait instance with mocked dependencies."""
160-
return HomeTrait(status_trait, maps_trait, map_content_trait, rooms_trait, device_cache, mock_web_api)
160+
rooms_trait._web_api = mock_web_api
161+
return HomeTrait(status_trait, maps_trait, map_content_trait, rooms_trait, device_cache)
161162

162163

163164
@pytest.fixture(autouse=True)

tests/devices/traits/v1/test_rooms.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
from roborock.data.containers import HomeDataRoom, NamedRoomMapping
89
from roborock.devices.device import RoborockDevice
910
from roborock.devices.traits.v1.rooms import RoomsTrait
1011
from roborock.devices.traits.v1.status import StatusTrait
@@ -70,3 +71,56 @@ async def test_refresh_rooms_trait(
7071
# Verify the RPC call was made correctly
7172
assert mock_rpc_channel.send_command.call_count == 1
7273
mock_rpc_channel.send_command.assert_any_call(RoborockCommand.GET_ROOM_MAPPING)
74+
75+
76+
def test_merge_home_data_rooms_appends_new_rooms(rooms_trait: RoomsTrait) -> None:
77+
"""Test merge_home_data_rooms appends new rooms without replacing known names."""
78+
rooms_trait.merge_home_data_rooms(
79+
[
80+
HomeDataRoom(id=2362048, name="Living Room"),
81+
HomeDataRoom(id=9999999, name="Office"),
82+
]
83+
)
84+
85+
home_data_rooms = {str(room.id): room.name for room in rooms_trait._home_data.rooms}
86+
assert home_data_rooms["2362048"] == "Example room 1"
87+
assert home_data_rooms["9999999"] == "Office"
88+
89+
90+
async def test_resolve_unknown_room_names_web_api_called_once(
91+
rooms_trait: RoomsTrait,
92+
web_api_client: AsyncMock,
93+
) -> None:
94+
"""Test unknown room IDs trigger one web lookup per iot_id."""
95+
web_api_client.get_rooms.return_value = [
96+
HomeDataRoom(id=2362048, name="Living Room"),
97+
]
98+
99+
room_map = {
100+
16: NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
101+
}
102+
await rooms_trait.resolve_unknown_room_names(room_map)
103+
assert room_map[16].name == "Living Room"
104+
105+
second_room_map = {
106+
16: NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
107+
}
108+
await rooms_trait.resolve_unknown_room_names(second_room_map)
109+
assert second_room_map[16].name == "Room 16"
110+
web_api_client.get_rooms.assert_called_once()
111+
112+
113+
async def test_resolve_unknown_room_names_falls_back_to_segment_id(
114+
rooms_trait: RoomsTrait,
115+
web_api_client: AsyncMock,
116+
) -> None:
117+
"""Test unresolved unknown names use Room {segment_id} fallback."""
118+
web_api_client.get_rooms.return_value = []
119+
room_map = {
120+
33: NamedRoomMapping(segment_id=33, iot_id="9999911", name="Unknown"),
121+
}
122+
123+
await rooms_trait.resolve_unknown_room_names(room_map)
124+
125+
assert room_map[33].name == "Room 33"
126+
web_api_client.get_rooms.assert_called_once()

0 commit comments

Comments
 (0)