Skip to content

Commit d11a52c

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

File tree

4 files changed

+102
-113
lines changed

4 files changed

+102
-113
lines changed

roborock/devices/traits/v1/home.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,13 @@ async def _refresh_map_info(self, map_info) -> CombinedMapInfo:
138138
# or if the room name isn't unknown.
139139
rooms[room.segment_id] = room
140140

141-
await self._rooms_trait.resolve_unknown_room_names(rooms)
141+
for segment_id, room in rooms.items():
142+
if room.name == "Unknown":
143+
rooms[segment_id] = NamedRoomMapping(
144+
segment_id=room.segment_id,
145+
iot_id=room.iot_id,
146+
name=f"Room {room.segment_id}",
147+
)
142148

143149
return CombinedMapInfo(
144150
map_flag=map_info.map_flag,

roborock/devices/traits/v1/rooms.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,26 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
3333

3434
command = RoborockCommand.GET_ROOM_MAPPING
3535

36-
def __init__(self, home_data: HomeData, web_api: UserWebApiClient | None = None) -> None:
36+
def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None:
3737
"""Initialize the RoomsTrait."""
3838
super().__init__()
3939
self._home_data = home_data
4040
self._web_api = web_api
4141
self._seen_unknown_room_iot_ids: set[str] = set()
4242

43+
async def refresh(self) -> None:
44+
"""Refresh room mappings and backfill unknown room names from the web API."""
45+
response = await self.rpc_channel.send_command(self.command)
46+
if not isinstance(response, list):
47+
raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
48+
49+
segment_pairs = _extract_segment_pairs(response)
50+
await self._populate_missing_home_data_rooms(segment_pairs)
51+
52+
new_data = self._parse_response(response)
53+
self._update_trait_values(new_data)
54+
_LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
55+
4356
@property
4457
def _iot_id_room_name_map(self) -> dict[str, str]:
4558
"""Returns a dictionary of Room IOT IDs to room names."""
@@ -73,30 +86,23 @@ def merge_home_data_rooms(self, rooms: list[HomeDataRoom]) -> None:
7386

7487
self._home_data.rooms = updated_rooms
7588

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}
89+
async def _populate_missing_home_data_rooms(self, segment_pairs: list[tuple[int, str]]) -> None:
90+
"""Load missing room names into home data for newly-seen unknown room ids."""
91+
name_map = self._iot_id_room_name_map
92+
unknown_room_iot_ids = {
93+
iot_id for _, iot_id in segment_pairs if name_map.get(iot_id, _DEFAULT_NAME) == _DEFAULT_NAME
94+
}
7995
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-
)
96+
if not new_unknown_room_iot_ids:
97+
return
98+
99+
try:
100+
web_rooms = await self._web_api.get_rooms()
101+
except Exception:
102+
_LOGGER.debug("Failed to fetch rooms from web API", exc_info=True)
103+
else:
104+
if web_rooms:
105+
self.merge_home_data_rooms(web_rooms)
100106

101107
self._seen_unknown_room_iot_ids.update(unknown_room_iot_ids)
102108

tests/devices/traits/v1/test_home.py

Lines changed: 41 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,10 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait:
140140

141141

142142
@pytest.fixture
143-
def mock_web_api() -> AsyncMock:
144-
"""Create a mock web API client."""
145-
mock = AsyncMock()
146-
mock.get_rooms.return_value = []
147-
return mock
143+
def mock_web_api(web_api_client: AsyncMock) -> AsyncMock:
144+
"""Alias the shared web API fixture for readability in this module."""
145+
web_api_client.get_rooms.return_value = []
146+
return web_api_client
148147

149148

150149
@pytest.fixture
@@ -154,10 +153,8 @@ def home_trait(
154153
map_content_trait: MapContentTrait,
155154
rooms_trait: RoomsTrait,
156155
device_cache: DeviceCache,
157-
mock_web_api: AsyncMock,
158156
) -> HomeTrait:
159157
"""Create a HomeTrait instance with mocked dependencies."""
160-
rooms_trait._web_api = mock_web_api
161158
return HomeTrait(status_trait, maps_trait, map_content_trait, rooms_trait, device_cache)
162159

163160

@@ -630,7 +627,6 @@ async def test_single_map_no_switching(
630627
async def test_refresh_map_info_room_override_and_addition_logic(
631628
home_trait: HomeTrait,
632629
rooms_trait: RoomsTrait,
633-
mock_web_api: AsyncMock,
634630
) -> None:
635631
"""Test the room override and addition logic in _refresh_map_info.
636632
@@ -671,9 +667,6 @@ async def test_refresh_map_info_room_override_and_addition_logic(
671667
NamedRoomMapping(segment_id=18, iot_id="2362041", name="Example room 3"), # Not in map_info, should be added
672668
]
673669

674-
# Mock web API to return empty rooms (no resolution)
675-
mock_web_api.get_rooms.return_value = []
676-
677670
# Mock rooms_trait.refresh to prevent actual device calls
678671
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
679672
result = await home_trait._refresh_map_info(map_info)
@@ -708,32 +701,29 @@ async def test_refresh_map_info_room_override_and_addition_logic(
708701

709702
async def test_get_rooms_resolves_unknown_rooms(
710703
home_trait: HomeTrait,
711-
rooms_trait: RoomsTrait,
704+
mock_rpc_channel: AsyncMock,
712705
mock_web_api: AsyncMock,
713706
) -> None:
714707
"""Test that get_rooms from web API resolves unknown room names."""
708+
room_mapping_data = [[16, "9999801"], [17, "9999802"]]
709+
mock_rpc_channel.send_command.side_effect = [room_mapping_data]
710+
715711
map_info = MultiMapsListMapInfo(
716712
map_flag=0,
717713
name="Test Map",
718714
rooms=[
719-
MultiMapsListRoom(id=16, iot_name_id="2362048", iot_name=None),
720-
MultiMapsListRoom(id=17, iot_name_id="2362044", iot_name=None),
715+
MultiMapsListRoom(id=16, iot_name_id="9999801", iot_name=None),
716+
MultiMapsListRoom(id=17, iot_name_id="9999802", iot_name=None),
721717
],
722718
)
723719

724-
rooms_trait.rooms = [
725-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
726-
NamedRoomMapping(segment_id=17, iot_id="2362044", name="Unknown"),
727-
]
728-
729720
# Web API returns fresh room names
730721
mock_web_api.get_rooms.return_value = [
731-
HomeDataRoom(id=2362048, name="Living Room"),
732-
HomeDataRoom(id=2362044, name="Kitchen"),
722+
HomeDataRoom(id=9999801, name="Living Room"),
723+
HomeDataRoom(id=9999802, name="Kitchen"),
733724
]
734725

735-
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
736-
result = await home_trait._refresh_map_info(map_info)
726+
result = await home_trait._refresh_map_info(map_info)
737727

738728
sorted_rooms = sorted(result.rooms, key=lambda r: r.segment_id)
739729
assert sorted_rooms[0].name == "Living Room"
@@ -743,28 +733,26 @@ async def test_get_rooms_resolves_unknown_rooms(
743733

744734
async def test_get_rooms_called_once_for_same_unknown_room(
745735
home_trait: HomeTrait,
746-
rooms_trait: RoomsTrait,
736+
mock_rpc_channel: AsyncMock,
747737
mock_web_api: AsyncMock,
748738
) -> None:
749739
"""Test that get_rooms is not re-called for an already-seen unknown room."""
740+
room_mapping_data = [[16, "9999701"]]
741+
mock_rpc_channel.send_command.side_effect = [room_mapping_data, room_mapping_data]
742+
750743
map_info = MultiMapsListMapInfo(
751744
map_flag=42,
752745
name="Test Map",
753746
rooms=[
754-
MultiMapsListRoom(id=16, iot_name_id="2362048", iot_name=None),
747+
MultiMapsListRoom(id=16, iot_name_id="9999701", iot_name=None),
755748
],
756749
)
757750

758-
rooms_trait.rooms = [
759-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
760-
]
761-
762751
# Web API returns empty (no resolution)
763752
mock_web_api.get_rooms.return_value = []
764753

765-
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
766-
result1 = await home_trait._refresh_map_info(map_info)
767-
result2 = await home_trait._refresh_map_info(map_info)
754+
result1 = await home_trait._refresh_map_info(map_info)
755+
result2 = await home_trait._refresh_map_info(map_info)
768756

769757
# Both calls should fall back to "Room 16"
770758
assert result1.rooms[0].name == "Room 16"
@@ -775,34 +763,27 @@ async def test_get_rooms_called_once_for_same_unknown_room(
775763

776764
async def test_get_rooms_called_again_for_new_unknown_room(
777765
home_trait: HomeTrait,
778-
rooms_trait: RoomsTrait,
766+
mock_rpc_channel: AsyncMock,
779767
mock_web_api: AsyncMock,
780768
) -> None:
781769
"""Test that get_rooms is called again when a new unknown room appears."""
770+
room_mapping_data_1 = [[16, "9999601"]]
771+
room_mapping_data_2 = [[16, "9999601"], [17, "9999602"]]
772+
mock_rpc_channel.send_command.side_effect = [room_mapping_data_1, room_mapping_data_2]
773+
782774
map_info = MultiMapsListMapInfo(
783775
map_flag=42,
784776
name="Test Map",
785777
rooms=[
786-
MultiMapsListRoom(id=16, iot_name_id="2362048", iot_name=None),
778+
MultiMapsListRoom(id=16, iot_name_id="9999601", iot_name=None),
787779
],
788780
)
789781

790-
rooms_trait.rooms = [
791-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
792-
]
793-
794782
# Web API returns empty (no resolution)
795783
mock_web_api.get_rooms.return_value = []
796784

797-
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
798-
result1 = await home_trait._refresh_map_info(map_info)
799-
800-
# Add a brand-new unknown room for the same map
801-
rooms_trait.rooms = [
802-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
803-
NamedRoomMapping(segment_id=17, iot_id="2362044", name="Unknown"),
804-
]
805-
result2 = await home_trait._refresh_map_info(map_info)
785+
result1 = await home_trait._refresh_map_info(map_info)
786+
result2 = await home_trait._refresh_map_info(map_info)
806787

807788
assert sorted(room.name for room in result1.rooms) == ["Room 16"]
808789
assert sorted(room.name for room in result2.rooms) == ["Room 16", "Room 17"]
@@ -811,56 +792,48 @@ async def test_get_rooms_called_again_for_new_unknown_room(
811792

812793
async def test_get_rooms_called_again_for_new_unknown_iot_id_same_segment(
813794
home_trait: HomeTrait,
814-
rooms_trait: RoomsTrait,
795+
mock_rpc_channel: AsyncMock,
815796
mock_web_api: AsyncMock,
816797
) -> None:
817798
"""Test that get_rooms is called again for a new unknown iot_id on the same segment."""
799+
room_mapping_data_1 = [[16, "9999501"]]
800+
room_mapping_data_2 = [[16, "9999502"]]
801+
mock_rpc_channel.send_command.side_effect = [room_mapping_data_1, room_mapping_data_2]
802+
818803
map_info = MultiMapsListMapInfo(
819804
map_flag=42,
820805
name="Test Map",
821806
)
822807

823-
rooms_trait.rooms = [
824-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
825-
]
826-
827808
mock_web_api.get_rooms.return_value = []
828809

829-
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
830-
await home_trait._refresh_map_info(map_info)
831-
832-
# Same segment, but now with a different iot_id.
833-
rooms_trait.rooms = [
834-
NamedRoomMapping(segment_id=16, iot_id="2362999", name="Unknown"),
835-
]
836-
await home_trait._refresh_map_info(map_info)
810+
await home_trait._refresh_map_info(map_info)
811+
await home_trait._refresh_map_info(map_info)
837812

838813
assert mock_web_api.get_rooms.call_count == 2
839814

840815

841816
async def test_get_rooms_failure_falls_back_to_room_segment_id(
842817
home_trait: HomeTrait,
843-
rooms_trait: RoomsTrait,
818+
mock_rpc_channel: AsyncMock,
844819
mock_web_api: AsyncMock,
845820
) -> None:
846821
"""Test that get_rooms failure gracefully falls back to 'Room {segment_id}'."""
822+
room_mapping_data = [[16, "9999401"]]
823+
mock_rpc_channel.send_command.side_effect = [room_mapping_data]
824+
847825
map_info = MultiMapsListMapInfo(
848826
map_flag=7,
849827
name="Test Map",
850828
rooms=[
851-
MultiMapsListRoom(id=16, iot_name_id="2362048", iot_name=None),
829+
MultiMapsListRoom(id=16, iot_name_id="9999401", iot_name=None),
852830
],
853831
)
854832

855-
rooms_trait.rooms = [
856-
NamedRoomMapping(segment_id=16, iot_id="2362048", name="Unknown"),
857-
]
858-
859833
# Web API raises an exception
860834
mock_web_api.get_rooms.side_effect = Exception("API error")
861835

862-
with patch.object(rooms_trait, "refresh", new_callable=AsyncMock):
863-
result = await home_trait._refresh_map_info(map_info)
836+
result = await home_trait._refresh_map_info(map_info)
864837

865838
assert result.rooms[0].name == "Room 16"
866839
mock_web_api.get_rooms.assert_called_once()

tests/devices/traits/v1/test_rooms.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,40 +87,44 @@ def test_merge_home_data_rooms_appends_new_rooms(rooms_trait: RoomsTrait) -> Non
8787
assert home_data_rooms["9999999"] == "Office"
8888

8989

90-
async def test_resolve_unknown_room_names_web_api_called_once(
90+
async def test_refresh_unknown_room_names_web_api_called_once(
9191
rooms_trait: RoomsTrait,
9292
web_api_client: AsyncMock,
93+
mock_rpc_channel: AsyncMock,
9394
) -> None:
9495
"""Test unknown room IDs trigger one web lookup per iot_id."""
9596
web_api_client.get_rooms.return_value = [
96-
HomeDataRoom(id=2362048, name="Living Room"),
97+
HomeDataRoom(id=9999911, name="Living Room"),
9798
]
9899

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"
100+
room_mapping_data = [[16, "9999911"]]
101+
mock_rpc_channel.send_command.side_effect = [room_mapping_data, room_mapping_data]
102+
103+
await rooms_trait.refresh()
104+
assert rooms_trait.rooms
105+
assert rooms_trait.rooms[0].name == "Living Room"
106+
107+
await rooms_trait.refresh()
108+
assert rooms_trait.rooms
109+
assert rooms_trait.rooms[0].name == "Living Room"
110110
web_api_client.get_rooms.assert_called_once()
111111

112112

113-
async def test_resolve_unknown_room_names_falls_back_to_segment_id(
113+
async def test_refresh_unknown_room_names_unresolved_keeps_unknown(
114114
rooms_trait: RoomsTrait,
115115
web_api_client: AsyncMock,
116+
mock_rpc_channel: AsyncMock,
116117
) -> None:
117-
"""Test unresolved unknown names use Room {segment_id} fallback."""
118+
"""Test unresolved unknown names stay unknown in RoomsTrait."""
118119
web_api_client.get_rooms.return_value = []
119-
room_map = {
120-
33: NamedRoomMapping(segment_id=33, iot_id="9999911", name="Unknown"),
121-
}
120+
room_mapping_data = [[33, "9999922"]]
121+
mock_rpc_channel.send_command.side_effect = [room_mapping_data, room_mapping_data]
122122

123-
await rooms_trait.resolve_unknown_room_names(room_map)
123+
await rooms_trait.refresh()
124+
assert rooms_trait.rooms
125+
assert rooms_trait.rooms[0] == NamedRoomMapping(segment_id=33, iot_id="9999922", name="Unknown")
124126

125-
assert room_map[33].name == "Room 33"
127+
await rooms_trait.refresh()
128+
assert rooms_trait.rooms
129+
assert rooms_trait.rooms[0] == NamedRoomMapping(segment_id=33, iot_id="9999922", name="Unknown")
126130
web_api_client.get_rooms.assert_called_once()

0 commit comments

Comments
 (0)