|
| 1 | +"""Trait for managing room mappings on Roborock devices.""" |
| 2 | + |
| 3 | +import logging |
| 4 | +from dataclasses import dataclass |
| 5 | + |
| 6 | +from roborock.containers import HomeData, RoborockBase, RoomMapping |
| 7 | +from roborock.devices.traits.v1 import common |
| 8 | +from roborock.roborock_typing import RoborockCommand |
| 9 | + |
| 10 | +_LOGGER = logging.getLogger(__name__) |
| 11 | + |
| 12 | +_DEFAULT_NAME = "Unknown" |
| 13 | + |
| 14 | + |
| 15 | +@dataclass |
| 16 | +class NamedRoomMapping(RoomMapping): |
| 17 | + """Dataclass representing a mapping of a room segment to a name. |
| 18 | +
|
| 19 | + The name information is not provided by the device directly, but is provided |
| 20 | + from the HomeData based on the iot_id from the room. |
| 21 | + """ |
| 22 | + |
| 23 | + name: str |
| 24 | + """The human-readable name of the room, if available.""" |
| 25 | + |
| 26 | + |
| 27 | +@dataclass |
| 28 | +class Rooms(RoborockBase): |
| 29 | + """Dataclass representing a collection of room mappings.""" |
| 30 | + |
| 31 | + rooms: list[NamedRoomMapping] | None = None |
| 32 | + """List of room mappings.""" |
| 33 | + |
| 34 | + @property |
| 35 | + def room_map(self) -> dict[int, NamedRoomMapping]: |
| 36 | + """Returns a mapping of segment_id to NamedRoomMapping.""" |
| 37 | + if self.rooms is None: |
| 38 | + return {} |
| 39 | + return {room.segment_id: room for room in self.rooms} |
| 40 | + |
| 41 | + |
| 42 | +class RoomsTrait(Rooms, common.V1TraitMixin): |
| 43 | + """Trait for managing the room mappings of Roborock devices.""" |
| 44 | + |
| 45 | + command = RoborockCommand.GET_ROOM_MAPPING |
| 46 | + |
| 47 | + def __init__(self, home_data: HomeData) -> None: |
| 48 | + """Initialize the RoomsTrait.""" |
| 49 | + super().__init__() |
| 50 | + self._home_data = home_data |
| 51 | + |
| 52 | + @property |
| 53 | + def _iot_id_room_name_map(self) -> dict[str, str]: |
| 54 | + """Returns a dictionary of Room IOT IDs to room names.""" |
| 55 | + return {str(room.id): room.name for room in self._home_data.rooms or ()} |
| 56 | + |
| 57 | + def _parse_response(self, response: common.V1ResponseData) -> Rooms: |
| 58 | + """Parse the response from the device into a list of NamedRoomMapping.""" |
| 59 | + if not isinstance(response, list): |
| 60 | + raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") |
| 61 | + name_map = self._iot_id_room_name_map |
| 62 | + segment_pairs = _extract_segment_pairs(response) |
| 63 | + return Rooms( |
| 64 | + rooms=[ |
| 65 | + NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, name=name_map.get(iot_id, _DEFAULT_NAME)) |
| 66 | + for segment_id, iot_id in segment_pairs |
| 67 | + ] |
| 68 | + ) |
| 69 | + |
| 70 | + |
| 71 | +def _extract_segment_pairs(response: list) -> list[tuple[int, str]]: |
| 72 | + """Extract segment_id and iot_id pairs from the response. |
| 73 | +
|
| 74 | + The response format can be either a flat list of [segment_id, iot_id] or a |
| 75 | + list of lists, where each inner list is a pair of [segment_id, iot_id]. This |
| 76 | + function normalizes the response into a list of (segment_id, iot_id) tuples |
| 77 | +
|
| 78 | + NOTE: We currently only partial samples of the room mapping formats, so |
| 79 | + improving test coverage with samples from a real device with this format |
| 80 | + would be helpful. |
| 81 | + """ |
| 82 | + if len(response) == 2 and not isinstance(response[0], list): |
| 83 | + segment_id, iot_id = response[0], response[1] |
| 84 | + return [(segment_id, iot_id)] |
| 85 | + |
| 86 | + segment_pairs: list[tuple[int, str]] = [] |
| 87 | + for part in response: |
| 88 | + if not isinstance(part, list) or len(part) < 2: |
| 89 | + _LOGGER.warning("Unexpected room mapping entry format: %r", part) |
| 90 | + continue |
| 91 | + segment_id, iot_id = part[0], part[1] |
| 92 | + segment_pairs.append((segment_id, iot_id)) |
| 93 | + return segment_pairs |
0 commit comments