33import logging
44from dataclasses import dataclass
55
6- from roborock .data import HomeData , NamedRoomMapping , RoborockBase
6+ from roborock .data import HomeData , HomeDataRoom , NamedRoomMapping , RoborockBase
77from roborock .devices .traits .v1 import common
88from roborock .roborock_typing import RoborockCommand
9+ from roborock .web_api import UserWebApiClient
910
1011_LOGGER = logging .getLogger (__name__ )
1112
12- _DEFAULT_NAME = "Unknown"
13-
1413
1514@dataclass
1615class Rooms (RoborockBase ):
@@ -32,50 +31,75 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
3231
3332 command = RoborockCommand .GET_ROOM_MAPPING
3433
35- def __init__ (self , home_data : HomeData ) -> None :
34+ def __init__ (self , home_data : HomeData , web_api : UserWebApiClient ) -> None :
3635 """Initialize the RoomsTrait."""
3736 super ().__init__ ()
3837 self ._home_data = home_data
38+ self ._web_api = web_api
39+ self ._discovered_iot_ids : set [str ] = set ()
3940
40- @property
41- def _iot_id_room_name_map (self ) -> dict [str , str ]:
42- """Returns a dictionary of Room IOT IDs to room names."""
43- return {str (room .id ): room .name for room in self ._home_data .rooms or ()}
44-
45- def _parse_response (self , response : common .V1ResponseData ) -> Rooms :
46- """Parse the response from the device into a list of NamedRoomMapping."""
41+ async def refresh (self ) -> None :
42+ """Refresh room mappings and backfill unknown room names from the web API."""
43+ response = await self .rpc_channel .send_command (self .command )
4744 if not isinstance (response , list ):
4845 raise ValueError (f"Unexpected RoomsTrait response format: { response !r} " )
49- name_map = self ._iot_id_room_name_map
50- segment_pairs = _extract_segment_pairs (response )
46+
47+ segment_map = _extract_segment_map (response )
48+ # Track all iot ids seen before. Refresh the room list when new ids are found.
49+ new_iot_ids = set (segment_map .values ()) - set (self ._home_data .rooms_map .keys ())
50+ if new_iot_ids - self ._discovered_iot_ids :
51+ _LOGGER .debug ("Refreshing room list to discover new room names" )
52+ if updated_rooms := await self ._refresh_rooms ():
53+ _LOGGER .debug ("Updating rooms: %s" , list (updated_rooms ))
54+ self ._home_data .rooms = updated_rooms
55+ self ._discovered_iot_ids .update (new_iot_ids )
56+
57+ new_data = self ._parse_rooms (segment_map , self ._home_data .rooms_name_map )
58+ self ._update_trait_values (new_data )
59+ _LOGGER .debug ("Refreshed %s: %s" , self .__class__ .__name__ , new_data )
60+
61+ @staticmethod
62+ def _parse_rooms (
63+ segment_map : dict [int , str ],
64+ name_map : dict [str , str ],
65+ ) -> Rooms :
66+ """Parse the response from the device into a list of NamedRoomMapping."""
5167 return Rooms (
5268 rooms = [
53- NamedRoomMapping (segment_id = segment_id , iot_id = iot_id , name = name_map .get (iot_id , _DEFAULT_NAME ))
54- for segment_id , iot_id in segment_pairs
69+ NamedRoomMapping (segment_id = segment_id , iot_id = iot_id , raw_name = name_map .get (iot_id ))
70+ for segment_id , iot_id in segment_map . items ()
5571 ]
5672 )
5773
74+ async def _refresh_rooms (self ) -> list [HomeDataRoom ]:
75+ """Fetch the latest rooms from the web API."""
76+ try :
77+ return await self ._web_api .get_rooms ()
78+ except Exception :
79+ _LOGGER .debug ("Failed to fetch rooms from web API" , exc_info = True )
80+ return []
81+
5882
59- def _extract_segment_pairs (response : list ) -> list [ tuple [ int , str ] ]:
60- """Extract segment_id and iot_id pairs from the response.
83+ def _extract_segment_map (response : list ) -> dict [ int , str ]:
84+ """Extract a segment_id -> iot_id mapping from the response.
6185
6286 The response format can be either a flat list of [segment_id, iot_id] or a
6387 list of lists, where each inner list is a pair of [segment_id, iot_id]. This
64- function normalizes the response into a list of ( segment_id, iot_id) tuples
88+ function normalizes the response into a dict of segment_id to iot_id.
6589
6690 NOTE: We currently only partial samples of the room mapping formats, so
6791 improving test coverage with samples from a real device with this format
6892 would be helpful.
6993 """
7094 if len (response ) == 2 and not isinstance (response [0 ], list ):
7195 segment_id , iot_id = response [0 ], response [1 ]
72- return [( segment_id , iot_id )]
96+ return { segment_id : str ( iot_id )}
7397
74- segment_pairs : list [ tuple [ int , str ]] = []
98+ segment_map : dict [ int , str ] = {}
7599 for part in response :
76100 if not isinstance (part , list ) or len (part ) < 2 :
77101 _LOGGER .warning ("Unexpected room mapping entry format: %r" , part )
78102 continue
79103 segment_id , iot_id = part [0 ], part [1 ]
80- segment_pairs . append (( segment_id , iot_id ) )
81- return segment_pairs
104+ segment_map [ segment_id ] = str ( iot_id )
105+ return segment_map
0 commit comments