roborock.devices.traits.v1.rooms
Trait for managing room mappings on Roborock devices.
1"""Trait for managing room mappings on Roborock devices.""" 2 3import logging 4from dataclasses import dataclass 5 6from roborock.data import HomeData, HomeDataRoom, NamedRoomMapping, RoborockBase 7from roborock.devices.traits.v1 import common 8from roborock.roborock_typing import RoborockCommand 9from roborock.web_api import UserWebApiClient 10 11_LOGGER = logging.getLogger(__name__) 12 13 14@dataclass 15class Rooms(RoborockBase): 16 """Dataclass representing a collection of room mappings.""" 17 18 rooms: list[NamedRoomMapping] | None = None 19 """List of room mappings.""" 20 21 @property 22 def room_map(self) -> dict[int, NamedRoomMapping]: 23 """Returns a mapping of segment_id to NamedRoomMapping.""" 24 if self.rooms is None: 25 return {} 26 return {room.segment_id: room for room in self.rooms} 27 28 def with_room_names(self, name_map: dict[str, str]) -> "Rooms": 29 """Create a new Rooms object with updated room names.""" 30 return Rooms( 31 rooms=[ 32 NamedRoomMapping( 33 segment_id=room.segment_id, 34 iot_id=room.iot_id, 35 raw_name=name_map.get(room.iot_id), 36 ) 37 for room in self.rooms or [] 38 ] 39 ) 40 41 42class RoomsConverter(common.V1TraitDataConverter): 43 """Converts response objects to Rooms.""" 44 45 def convert(self, response: common.V1ResponseData) -> Rooms: 46 """Parse the response from the device into a list of NamedRoomMapping.""" 47 if not isinstance(response, list): 48 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 49 segment_map = self.extract_segment_map(response) 50 return Rooms( 51 rooms=[NamedRoomMapping(segment_id=segment_id, iot_id=iot_id) for segment_id, iot_id in segment_map.items()] 52 ) 53 54 @staticmethod 55 def extract_segment_map(response: list) -> dict[int, str]: 56 """Extract a segment_id -> iot_id mapping from the response. 57 58 The response format can be either a flat list of [segment_id, iot_id] or a 59 list of lists, where each inner list is a pair of [segment_id, iot_id]. This 60 function normalizes the response into a dict of segment_id to iot_id. 61 62 NOTE: We currently only partial samples of the room mapping formats, so 63 improving test coverage with samples from a real device with this format 64 would be helpful. 65 """ 66 if len(response) == 2 and not isinstance(response[0], list): 67 segment_id, iot_id = response[0], response[1] 68 return {segment_id: str(iot_id)} 69 70 segment_map: dict[int, str] = {} 71 for part in response: 72 if not isinstance(part, list) or len(part) < 2: 73 _LOGGER.warning("Unexpected room mapping entry format: %r", part) 74 continue 75 segment_id, iot_id = part[0], part[1] 76 segment_map[segment_id] = str(iot_id) 77 return segment_map 78 79 80class RoomsTrait(Rooms, common.V1TraitMixin): 81 """Trait for managing the room mappings of Roborock devices.""" 82 83 command = RoborockCommand.GET_ROOM_MAPPING 84 converter = RoomsConverter() 85 86 def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None: 87 """Initialize the RoomsTrait.""" 88 super().__init__() 89 self._home_data = home_data 90 self._web_api = web_api 91 self._discovered_iot_ids: set[str] = set() 92 93 async def refresh(self) -> None: 94 """Refresh room mappings and backfill unknown room names from the web API.""" 95 response = await self.rpc_channel.send_command(self.command) 96 if not isinstance(response, list): 97 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 98 99 segment_map = RoomsConverter.extract_segment_map(response) 100 # Track all iot ids seen before. Refresh the room list when new ids are found. 101 new_iot_ids = set(segment_map.values()) - set(self._home_data.rooms_map.keys()) 102 if new_iot_ids - self._discovered_iot_ids: 103 _LOGGER.debug("Refreshing room list to discover new room names") 104 if updated_rooms := await self._refresh_rooms(): 105 _LOGGER.debug("Updating rooms: %s", list(updated_rooms)) 106 self._home_data.rooms = updated_rooms 107 self._discovered_iot_ids.update(new_iot_ids) 108 109 rooms = self.converter.convert(response) 110 rooms = rooms.with_room_names(self._home_data.rooms_name_map) 111 common.merge_trait_values(self, rooms) 112 113 async def _refresh_rooms(self) -> list[HomeDataRoom]: 114 """Fetch the latest rooms from the web API.""" 115 try: 116 return await self._web_api.get_rooms() 117 except Exception: 118 _LOGGER.debug("Failed to fetch rooms from web API", exc_info=True) 119 return []
15@dataclass 16class Rooms(RoborockBase): 17 """Dataclass representing a collection of room mappings.""" 18 19 rooms: list[NamedRoomMapping] | None = None 20 """List of room mappings.""" 21 22 @property 23 def room_map(self) -> dict[int, NamedRoomMapping]: 24 """Returns a mapping of segment_id to NamedRoomMapping.""" 25 if self.rooms is None: 26 return {} 27 return {room.segment_id: room for room in self.rooms} 28 29 def with_room_names(self, name_map: dict[str, str]) -> "Rooms": 30 """Create a new Rooms object with updated room names.""" 31 return Rooms( 32 rooms=[ 33 NamedRoomMapping( 34 segment_id=room.segment_id, 35 iot_id=room.iot_id, 36 raw_name=name_map.get(room.iot_id), 37 ) 38 for room in self.rooms or [] 39 ] 40 )
Dataclass representing a collection of room mappings.
22 @property 23 def room_map(self) -> dict[int, NamedRoomMapping]: 24 """Returns a mapping of segment_id to NamedRoomMapping.""" 25 if self.rooms is None: 26 return {} 27 return {room.segment_id: room for room in self.rooms}
Returns a mapping of segment_id to NamedRoomMapping.
29 def with_room_names(self, name_map: dict[str, str]) -> "Rooms": 30 """Create a new Rooms object with updated room names.""" 31 return Rooms( 32 rooms=[ 33 NamedRoomMapping( 34 segment_id=room.segment_id, 35 iot_id=room.iot_id, 36 raw_name=name_map.get(room.iot_id), 37 ) 38 for room in self.rooms or [] 39 ] 40 )
Create a new Rooms object with updated room names.
Inherited Members
43class RoomsConverter(common.V1TraitDataConverter): 44 """Converts response objects to Rooms.""" 45 46 def convert(self, response: common.V1ResponseData) -> Rooms: 47 """Parse the response from the device into a list of NamedRoomMapping.""" 48 if not isinstance(response, list): 49 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 50 segment_map = self.extract_segment_map(response) 51 return Rooms( 52 rooms=[NamedRoomMapping(segment_id=segment_id, iot_id=iot_id) for segment_id, iot_id in segment_map.items()] 53 ) 54 55 @staticmethod 56 def extract_segment_map(response: list) -> dict[int, str]: 57 """Extract a segment_id -> iot_id mapping from the response. 58 59 The response format can be either a flat list of [segment_id, iot_id] or a 60 list of lists, where each inner list is a pair of [segment_id, iot_id]. This 61 function normalizes the response into a dict of segment_id to iot_id. 62 63 NOTE: We currently only partial samples of the room mapping formats, so 64 improving test coverage with samples from a real device with this format 65 would be helpful. 66 """ 67 if len(response) == 2 and not isinstance(response[0], list): 68 segment_id, iot_id = response[0], response[1] 69 return {segment_id: str(iot_id)} 70 71 segment_map: dict[int, str] = {} 72 for part in response: 73 if not isinstance(part, list) or len(part) < 2: 74 _LOGGER.warning("Unexpected room mapping entry format: %r", part) 75 continue 76 segment_id, iot_id = part[0], part[1] 77 segment_map[segment_id] = str(iot_id) 78 return segment_map
Converts response objects to Rooms.
46 def convert(self, response: common.V1ResponseData) -> Rooms: 47 """Parse the response from the device into a list of NamedRoomMapping.""" 48 if not isinstance(response, list): 49 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 50 segment_map = self.extract_segment_map(response) 51 return Rooms( 52 rooms=[NamedRoomMapping(segment_id=segment_id, iot_id=iot_id) for segment_id, iot_id in segment_map.items()] 53 )
Parse the response from the device into a list of NamedRoomMapping.
55 @staticmethod 56 def extract_segment_map(response: list) -> dict[int, str]: 57 """Extract a segment_id -> iot_id mapping from the response. 58 59 The response format can be either a flat list of [segment_id, iot_id] or a 60 list of lists, where each inner list is a pair of [segment_id, iot_id]. This 61 function normalizes the response into a dict of segment_id to iot_id. 62 63 NOTE: We currently only partial samples of the room mapping formats, so 64 improving test coverage with samples from a real device with this format 65 would be helpful. 66 """ 67 if len(response) == 2 and not isinstance(response[0], list): 68 segment_id, iot_id = response[0], response[1] 69 return {segment_id: str(iot_id)} 70 71 segment_map: dict[int, str] = {} 72 for part in response: 73 if not isinstance(part, list) or len(part) < 2: 74 _LOGGER.warning("Unexpected room mapping entry format: %r", part) 75 continue 76 segment_id, iot_id = part[0], part[1] 77 segment_map[segment_id] = str(iot_id) 78 return segment_map
Extract a segment_id -> iot_id mapping from the response.
The response format can be either a flat list of [segment_id, iot_id] or a list of lists, where each inner list is a pair of [segment_id, iot_id]. This function normalizes the response into a dict of segment_id to iot_id.
NOTE: We currently only partial samples of the room mapping formats, so improving test coverage with samples from a real device with this format would be helpful.
81class RoomsTrait(Rooms, common.V1TraitMixin): 82 """Trait for managing the room mappings of Roborock devices.""" 83 84 command = RoborockCommand.GET_ROOM_MAPPING 85 converter = RoomsConverter() 86 87 def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None: 88 """Initialize the RoomsTrait.""" 89 super().__init__() 90 self._home_data = home_data 91 self._web_api = web_api 92 self._discovered_iot_ids: set[str] = set() 93 94 async def refresh(self) -> None: 95 """Refresh room mappings and backfill unknown room names from the web API.""" 96 response = await self.rpc_channel.send_command(self.command) 97 if not isinstance(response, list): 98 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 99 100 segment_map = RoomsConverter.extract_segment_map(response) 101 # Track all iot ids seen before. Refresh the room list when new ids are found. 102 new_iot_ids = set(segment_map.values()) - set(self._home_data.rooms_map.keys()) 103 if new_iot_ids - self._discovered_iot_ids: 104 _LOGGER.debug("Refreshing room list to discover new room names") 105 if updated_rooms := await self._refresh_rooms(): 106 _LOGGER.debug("Updating rooms: %s", list(updated_rooms)) 107 self._home_data.rooms = updated_rooms 108 self._discovered_iot_ids.update(new_iot_ids) 109 110 rooms = self.converter.convert(response) 111 rooms = rooms.with_room_names(self._home_data.rooms_name_map) 112 common.merge_trait_values(self, rooms) 113 114 async def _refresh_rooms(self) -> list[HomeDataRoom]: 115 """Fetch the latest rooms from the web API.""" 116 try: 117 return await self._web_api.get_rooms() 118 except Exception: 119 _LOGGER.debug("Failed to fetch rooms from web API", exc_info=True) 120 return []
Trait for managing the room mappings of Roborock devices.
87 def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None: 88 """Initialize the RoomsTrait.""" 89 super().__init__() 90 self._home_data = home_data 91 self._web_api = web_api 92 self._discovered_iot_ids: set[str] = set()
Initialize the RoomsTrait.
The RoborockCommand used to fetch the trait data from the device (internal only).
The converter used to parse the response from the device (internal only).
94 async def refresh(self) -> None: 95 """Refresh room mappings and backfill unknown room names from the web API.""" 96 response = await self.rpc_channel.send_command(self.command) 97 if not isinstance(response, list): 98 raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") 99 100 segment_map = RoomsConverter.extract_segment_map(response) 101 # Track all iot ids seen before. Refresh the room list when new ids are found. 102 new_iot_ids = set(segment_map.values()) - set(self._home_data.rooms_map.keys()) 103 if new_iot_ids - self._discovered_iot_ids: 104 _LOGGER.debug("Refreshing room list to discover new room names") 105 if updated_rooms := await self._refresh_rooms(): 106 _LOGGER.debug("Updating rooms: %s", list(updated_rooms)) 107 self._home_data.rooms = updated_rooms 108 self._discovered_iot_ids.update(new_iot_ids) 109 110 rooms = self.converter.convert(response) 111 rooms = rooms.with_room_names(self._home_data.rooms_name_map) 112 common.merge_trait_values(self, rooms)
Refresh room mappings and backfill unknown room names from the web API.