-
-
Notifications
You must be signed in to change notification settings - Fork 862
馃Ч add support for xiaomi.vacuum.d103cn #2868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| """Support for Xiaomi vacuums.""" | ||
| import logging | ||
| import asyncio | ||
| import json | ||
| from datetime import timedelta | ||
|
|
||
| from homeassistant.components.vacuum import ( # noqa: F401 | ||
|
|
@@ -49,7 +50,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= | |
| for srv in spec.get_services(ENTITY_DOMAIN, 'mopping_machine'): | ||
| if not srv.get_property('status'): | ||
| continue | ||
| if model in MIOT_LOCAL_MODELS: | ||
| if model in ['xiaomi.vacuum.d103cn']: | ||
| entities.append(MiotXiaomiVacuumEntity(config, srv)) | ||
| elif model in MIOT_LOCAL_MODELS: | ||
| entities.append(MiotVacuumEntity(config, srv)) | ||
| elif 'roborock.' in model or 'rockrobo.' in model: | ||
| entities.append(MiotRoborockVacuumEntity(config, srv)) | ||
|
|
@@ -399,3 +402,222 @@ async def async_clean_zones(self, zones, repeats=1): | |
| async def async_clean_point(self, point): | ||
| await self.async_miio_command('set_uploadmap', [0]) | ||
| return await self.async_miio_command('set_pointclean', [1, *point]) | ||
|
|
||
|
|
||
| class MiotXiaomiVacuumEntity(MiotVacuumEntity): | ||
| def __init__(self, config: dict, miot_service: MiotService): | ||
| super().__init__(config, miot_service) | ||
| self._act_room_sweep = miot_service.get_action('start_room_sweep', 'start_room_clean') | ||
| if self._act_room_sweep: | ||
| self._supported_features |= VacuumEntityFeature.SEND_COMMAND | ||
|
|
||
| async def async_added_to_hass(self): | ||
| await super().async_added_to_hass() | ||
| if not self._act_room_sweep: | ||
| return | ||
| await self._setup_room_buttons() | ||
|
|
||
| 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 | ||
| entities = [] | ||
| for rid, rname in rooms: | ||
| sub = f'room_{rid}' | ||
| entity = 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] = entity | ||
| entities.append(entity) | ||
| if entities: | ||
| add_buttons(entities, update_before_add=False) | ||
| self.logger.info('Room buttons added: %s', rooms) | ||
|
|
||
| async def _get_rooms(self): | ||
| rooms = await self._get_device_rooms() | ||
| if not rooms: | ||
| rooms = await self._get_cloud_rooms() | ||
| if not rooms: | ||
| return None | ||
| seen = set() | ||
| deduped = [] | ||
| for rid, name in rooms: | ||
| entry = self._normalize_room_entry(rid, name) | ||
| if not entry or entry[0] in seen: | ||
| continue | ||
| seen.add(entry[0]) | ||
| deduped.append(entry) | ||
| if not deduped: | ||
| return None | ||
| self._state_attrs['room_mapping'] = deduped | ||
| return deduped | ||
|
|
||
| async def _get_device_rooms(self): | ||
| # Try to read room data from device props | ||
| props = self.device.props or {} | ||
| for key in ('room_information', 'vacuum_extend.room_information', | ||
| 'room_ids', 'vacuum.room_ids', | ||
| 'vacuum_room_ids', 'clean_room_ids'): | ||
| val = props.get(key) | ||
| if val: | ||
| rooms = self._parse_room_data(val) | ||
| if rooms: | ||
| return rooms | ||
| # Try to fetch room_information directly from device | ||
| prop = self._miot_service.get_property('room_information') | ||
| if prop: | ||
| try: | ||
| mapping = {prop.full_name: {'siid': self._miot_service.iid, 'piid': prop.iid}} | ||
| vals = await self.device.async_get_properties(mapping, update_entity=False) | ||
| if vals: | ||
| val = vals.get(prop.full_name) | ||
| if val: | ||
| rooms = self._parse_room_data(val) | ||
| if rooms: | ||
| return rooms | ||
| except Exception: | ||
| pass | ||
| # Try miio get_room_mapping command (like Roborock) | ||
| if self.miot_device: | ||
| try: | ||
| result = await self.miot_device.async_send('get_room_mapping') | ||
| if result and result != 'unknown_method': | ||
| return await self._process_room_mapping(result) | ||
| except Exception: | ||
| pass | ||
| return None | ||
|
|
||
| async def _get_cloud_rooms(self): | ||
| if not self.xiaomi_cloud: | ||
| return None | ||
| homes = await self.xiaomi_cloud.async_get_homerooms() or [] | ||
| if not homes: | ||
| return None | ||
| # Find the home that this device belongs to | ||
| device_did = self.device.did | ||
| target_home_id = None | ||
| if device_did: | ||
| devices = await self.xiaomi_cloud.async_get_devices_by_key('did') or {} | ||
| dev = devices.get(device_did) | ||
| if dev: | ||
| target_home_id = dev.get('home_id') | ||
| rooms = [] | ||
| for home in homes: | ||
| if target_home_id is not None and home.get('id') != target_home_id: | ||
| continue | ||
| for room in home.get('roomlist', []): | ||
| if not isinstance(room, dict): | ||
| continue | ||
| entry = self._normalize_room_entry(room.get('id'), room.get('name')) | ||
| if entry: | ||
| rooms.append(entry) | ||
| return rooms or None | ||
|
|
||
| @staticmethod | ||
| def _normalize_room_id(rid): | ||
| if rid is None: | ||
| return None | ||
| rid = str(rid).strip() | ||
| if not rid or rid == 'None': | ||
| return None | ||
| return rid | ||
|
|
||
| @classmethod | ||
| def _normalize_room_name(cls, name, rid): | ||
| rid = cls._normalize_room_id(rid) or str(rid) | ||
| if name is None: | ||
| return f'Room {rid}' | ||
| name = str(name).strip() | ||
| if not name or name == 'None': | ||
| return f'Room {rid}' | ||
| return name | ||
|
|
||
| @classmethod | ||
| def _normalize_room_entry(cls, rid, name=None): | ||
| rid = cls._normalize_room_id(rid) | ||
| if not rid: | ||
| return None | ||
| return rid, cls._normalize_room_name(name, rid) | ||
|
|
||
| @classmethod | ||
| def _parse_room_data(cls, 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 item is None: | ||
| continue | ||
| if isinstance(item, (list, tuple)): | ||
| if not item: | ||
| continue | ||
| entry = cls._normalize_room_entry( | ||
| item[0], item[1] if len(item) > 1 else None, | ||
| ) | ||
| elif isinstance(item, dict): | ||
| rid = item.get('id') | ||
| if rid is None: | ||
| rid = item.get('ID') | ||
| entry = cls._normalize_room_entry( | ||
| rid, item.get('name') or item.get('Name'), | ||
| ) | ||
| else: | ||
| entry = cls._normalize_room_entry(item) | ||
| if entry: | ||
| rooms.append(entry) | ||
| elif isinstance(data, dict): | ||
| for rid, name in data.items(): | ||
| entry = cls._normalize_room_entry(rid, name) | ||
| if entry: | ||
| rooms.append(entry) | ||
| return rooms or None | ||
|
Comment on lines
+523
to
+585
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When parsing room data from dictionaries, if both @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', []): | ||
| if not isinstance(room, dict): | ||
| continue | ||
| cloud_id = room.get('id') | ||
| if cloud_id is not None: | ||
| cloud_rooms[cloud_id] = room.get('name') | ||
| except Exception: | ||
| pass | ||
| rooms = [] | ||
| for item in data: | ||
| if item is None or not isinstance(item, (list, tuple)) or len(item) < 2: | ||
| continue | ||
| seg_id = self._normalize_room_id(item[0]) | ||
| if not seg_id: | ||
| continue | ||
| name = None | ||
| if len(item) > 2 and item[2] is not None: | ||
| name = item[2] | ||
| elif item[1] is not None: | ||
| name = cloud_rooms.get(item[1]) | ||
| rooms.append((seg_id, self._normalize_room_name(name, seg_id))) | ||
| return rooms or None | ||
|
Comment on lines
+587
to
+616
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve robustness when processing room mapping by explicitly checking for 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 None |
||
|
|
||
| async def async_start_room_sweep(self, room_id, **kwargs): | ||
| room_id = self._normalize_room_id(room_id) | ||
| if not self._act_room_sweep or not room_id: | ||
| return False | ||
| params = self._act_room_sweep.in_params([str(room_id)]) | ||
| return await self.async_call_action(self._act_room_sweep, params=params) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling
add_buttonsinside 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 calladd_buttonsonce after the loop.