馃Ч add support for xiaomi.vacuum.d103cn - #2868
Conversation
Add device customizes with optimized polling and room sweep buttons via MiotXiaomiVacuumEntity for per-room cleaning.
There was a problem hiding this comment.
Code Review
This pull request introduces support for the xiaomi.vacuum.d103cn vacuum model. It adds custom device configurations and implements a new MiotXiaomiVacuumEntity class to handle dynamic room button generation for room-specific sweeping. The room data is retrieved via device properties, direct commands, or Xiaomi Cloud integration. The review feedback highlights opportunities to optimize performance by batching the registration of room buttons instead of adding them individually in a loop, and suggests improving the robustness of room data parsing and mapping by explicitly handling potential None values to prevent invalid configurations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async def _setup_room_buttons(self): | ||
| rooms = await self._get_rooms() | ||
| if not rooms: | ||
| return | ||
| add_buttons = self.device.entry.adders.get('button') | ||
| if not add_buttons: | ||
| return | ||
| from .button import ButtonSubEntity | ||
| for rid, rname in rooms: | ||
| sub = f'room_{rid}' | ||
| self._subs[sub] = ButtonSubEntity(self, sub, option={ | ||
| 'name': f'{self.device_name} {rname}', | ||
| 'async_press_action': self.async_start_room_sweep, | ||
| 'press_kwargs': {'room_id': rid}, | ||
| 'state_attrs': {'room_id': rid, 'room_name': rname}, | ||
| }) | ||
| add_buttons([self._subs[sub]], update_before_add=False) | ||
| self.logger.info('Room buttons added: %s', rooms) |
There was a problem hiding this comment.
Calling add_buttons inside the loop for each room is inefficient as it triggers multiple update cycles in Home Assistant. It is highly recommended to batch all the new button entities into a list and call add_buttons once after the loop.
async def _setup_room_buttons(self):
rooms = await self._get_rooms()
if not rooms:
return
add_buttons = self.device.entry.adders.get('button')
if not add_buttons:
return
from .button import ButtonSubEntity
new_buttons = []
for rid, rname in rooms:
sub = f'room_{rid}'
button = ButtonSubEntity(self, sub, option={
'name': f'{self.device_name} {rname}',
'async_press_action': self.async_start_room_sweep,
'press_kwargs': {'room_id': rid},
'state_attrs': {'room_id': rid, 'room_name': rname},
})
self._subs[sub] = button
new_buttons.append(button)
if new_buttons:
add_buttons(new_buttons, update_before_add=False)
self.logger.info('Room buttons added: %s', rooms)| @staticmethod | ||
| def _parse_room_data(data): | ||
| if not data: | ||
| return None | ||
| if isinstance(data, str): | ||
| try: | ||
| data = json.loads(data) | ||
| except (json.JSONDecodeError, TypeError): | ||
| return None | ||
| rooms = [] | ||
| if isinstance(data, list): | ||
| for item in data: | ||
| if isinstance(item, (list, tuple)): | ||
| rid = str(item[0]) | ||
| name = str(item[1]) if len(item) > 1 else f'Room {rid}' | ||
| rooms.append((rid, name)) | ||
| elif isinstance(item, dict): | ||
| rid = str(item.get('id') or item.get('ID')) | ||
| name = str(item.get('name') or item.get('Name') or f'Room {rid}') | ||
| rooms.append((rid, name)) | ||
| else: | ||
| rid = str(item) | ||
| rooms.append((rid, f'Room {rid}')) | ||
| elif isinstance(data, dict): | ||
| for rid, name in data.items(): | ||
| rid = str(rid) | ||
| rooms.append((rid, str(name) if name else f'Room {rid}')) | ||
| return rooms or None |
There was a problem hiding this comment.
When parsing room data from dictionaries, if both id and ID keys are missing, rid will resolve to the string 'None'. This can lead to invalid room buttons and errors when trying to sweep. Additionally, we should guard against None values in lists and dictionary keys to ensure robust parsing.
@staticmethod
def _parse_room_data(data):
if not data:
return None
if isinstance(data, str):
try:
data = json.loads(data)
except (json.JSONDecodeError, TypeError):
return None
rooms = []
if isinstance(data, list):
for item in data:
if isinstance(item, (list, tuple)):
if item and item[0] is not None:
rid = str(item[0])
name = str(item[1]) if len(item) > 1 and item[1] is not None else f'Room {rid}'
rooms.append((rid, name))
elif isinstance(item, dict):
rid = item.get('id') or item.get('ID')
if rid is not None:
rid = str(rid)
name = str(item.get('name') or item.get('Name') or f'Room {rid}')
rooms.append((rid, name))
else:
if item is not None:
rid = str(item)
rooms.append((rid, f'Room {rid}'))
elif isinstance(data, dict):
for rid, name in data.items():
if rid is not None:
rid = str(rid)
rooms.append((rid, str(name) if name else f'Room {rid}'))
return rooms or None| async def _process_room_mapping(self, data): | ||
| if not data or not isinstance(data, list): | ||
| return None | ||
| cloud_rooms = {} | ||
| if self.xiaomi_cloud: | ||
| try: | ||
| homes = await self.xiaomi_cloud.async_get_homerooms() or [] | ||
| for home in homes: | ||
| for room in home.get('roomlist', []): | ||
| cloud_rooms[room['id']] = room['name'] | ||
| except Exception: | ||
| pass | ||
| rooms = [] | ||
| for item in data: | ||
| if isinstance(item, (list, tuple)) and len(item) >= 2: | ||
| seg_id = str(item[0]) | ||
| cloud_id = item[1] | ||
| name = str(item[2]) if len(item) > 2 else cloud_rooms.get(cloud_id, f'Room {seg_id}') | ||
| rooms.append((seg_id, name)) | ||
| return rooms or None |
There was a problem hiding this comment.
Improve robustness when processing room mapping by explicitly checking for None values in the list items and fallback names. This prevents potential string representation of 'None' from being used as room names or IDs.
async def _process_room_mapping(self, data):
if not data or not isinstance(data, list):
return None
cloud_rooms = {}
if self.xiaomi_cloud:
try:
homes = await self.xiaomi_cloud.async_get_homerooms() or []
for home in homes:
for room in home.get('roomlist', []):
r_id = room.get('id')
r_name = room.get('name')
if r_id is not None and r_name is not None:
cloud_rooms[r_id] = r_name
except Exception:
pass
rooms = []
for item in data:
if isinstance(item, (list, tuple)) and len(item) >= 2:
if item[0] is not None:
seg_id = str(item[0])
cloud_id = item[1]
name = None
if len(item) > 2 and item[2] is not None:
name = str(item[2])
if not name and cloud_id is not None:
name = cloud_rooms.get(cloud_id)
if not name:
name = f'Room {seg_id}'
rooms.append((seg_id, name))
return rooms or NoneRefactor room button creation and normalization methods to improve robustness against invalid data. Added checks for room entries and streamlined the addition of room buttons for better performance.
xiaomi.vacuum.d103cndevice customizes with chunked polling, button actions, and sensor properties (cleaning_area,cleaning_time,dry_left_time)MiotXiaomiVacuumEntitywith per-room cleaning viastart_room_sweep/start_room_cleanMIOT actionget_room_mapping, or Xiaomi Cloud fallback