diff --git a/custom_components/xiaomi_miot/core/device_customizes.py b/custom_components/xiaomi_miot/core/device_customizes.py index 7d34e3ea6..e5f08f511 100644 --- a/custom_components/xiaomi_miot/core/device_customizes.py +++ b/custom_components/xiaomi_miot/core/device_customizes.py @@ -1110,6 +1110,9 @@ 'sensor_properties': 'remain_clean_time,fault,filter_left_time,no_water_time', 'select_properties': 'mode', }, + 'mrbond.airer.m1s': { + 'miot_type': 'urn:miot-spec-v2:device:airer:0000A00D:mrbond-m1pro:1', + }, 'mrbond.airer.m53pro': { 'position_reverse': False, 'sensor_properties': 'fault,left_time', @@ -1118,6 +1121,10 @@ 'fan_properties': '', 'chunk_properties': 1, }, + 'mrbond.airer.*': { + 'main_miot_services': 'airer', + 'parallel_updates': 1, + }, 'msj.f_washer.m2': { 'chunk_properties': 1, 'button_actions': 'start_wash,pause,drain,pause_drain', diff --git a/custom_components/xiaomi_miot/core/miio2miot.py b/custom_components/xiaomi_miot/core/miio2miot.py index c375f68ee..b6b30bc4f 100644 --- a/custom_components/xiaomi_miot/core/miio2miot.py +++ b/custom_components/xiaomi_miot/core/miio2miot.py @@ -79,7 +79,8 @@ def get_miio_props(self, device): except (DeviceException, OSError) as exc: if is_offline_exception(exc): raise exc - _LOGGER.error('%s: Got MiioException: %s while %s(%s)', self.model, exc, c['method'], pms) + if not c.get('ignore_error'): + _LOGGER.error('%s: Got MiioException: %s while %s(%s)', self.model, exc, c['method'], pms) continue kls = c.get('values', []) if kls is True: diff --git a/custom_components/xiaomi_miot/core/miio2miot_specs.py b/custom_components/xiaomi_miot/core/miio2miot_specs.py index a6341e75a..3155f814e 100644 --- a/custom_components/xiaomi_miot/core/miio2miot_specs.py +++ b/custom_components/xiaomi_miot/core/miio2miot_specs.py @@ -786,6 +786,27 @@ def cbk(prop, params, props, **kwargs): }, } }, + 'mrbond.airer.m1pro': { + 'chunk_properties': 1, + 'entity_attrs': ['airer_location'], + 'miio_commands': [ + {'method': 'get_prop', 'values': True, 'ignore_error': True, 'params': ['drytime']}, + {'method': 'get_prop', 'values': True, 'ignore_error': True, 'params': ['airer_location']}, + ], + 'miio_specs': { + 'prop.2.1': {'prop': 'motor', 'setter': True}, + 'prop.2.2': { + 'prop': 'dry', + 'setter': True, + 'template': '{{ value|int > 0 }}', + 'set_template': '{{ [value|int] }}', + }, + 'prop.2.3': {'prop': 'dry', 'setter': 'set_dry'}, + 'prop.2.4': {'prop': 'dry', 'template': '{{ props.drytime|default(0)|int }}'}, + 'prop.3.1': {'prop': 'led', 'setter': True, 'set_template': '{{ [value|int] }}'}, + } + }, + 'mrbond.airer.m1s': 'mrbond.airer.m1pro', 'nwt.derh.wdh318efw1': { 'chunk_properties': 1, diff --git a/custom_components/xiaomi_miot/cover.py b/custom_components/xiaomi_miot/cover.py index fb1e8389c..5f1982b7c 100644 --- a/custom_components/xiaomi_miot/cover.py +++ b/custom_components/xiaomi_miot/cover.py @@ -65,10 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for srv in spec.get_services(ENTITY_DOMAIN, 'curtain', 'airer', 'window_opener', 'motor_controller'): if not srv.get_property('motor_control'): continue - if model in ['mrbond.airer.m1s', 'mrbond.airer.m1pro']: - entities.append(MrBondAirerProEntity(config)) - else: - entities.append(MiotCoverEntity(config, srv)) + entities.append(MiotCoverEntity(config, srv)) for entity in entities: hass.data[DOMAIN]['entities'][entity.unique_id] = entity async_add_entities(entities, update_before_add=True) @@ -361,277 +358,3 @@ def stop_cover(self, **kwargs): if val is not None: return self.set_parent_property(val) raise NotImplementedError() - - -class MiioCoverEntity(MiioEntity, CoverEntity): - def __init__(self, name, device, **kwargs): - kwargs.setdefault('logger', _LOGGER) - super().__init__(name, device, **kwargs) - self._position = None - self._set_position = None - self._unsub_listener_cover = None - self._is_opening = False - self._is_closing = False - self._requested_closing = True - - @property - def current_cover_position(self): - return self._position - - @property - def is_closed(self): - if self._position is not None: - return self._position <= 0 - return None - - @property - def is_closing(self): - return self._is_closing - - @property - def is_opening(self): - return self._is_opening - - def open_cover(self, **kwargs): - pass - - def close_cover(self, **kwargs): - pass - - @callback - def _listen_cover(self): - if self._unsub_listener_cover is None: - self._unsub_listener_cover = async_track_utc_time_change( - self.hass, self._time_changed_cover # noqa - ) - - async def _time_changed_cover(self, now): - if self._requested_closing: - self._position -= 10 if self._position >= 10 else 0 - else: - self._position += 10 if self._position <= 90 else 0 - if self._position in (100, 0, self._set_position): - self._unsub_listener_cover() - self._unsub_listener_cover = None - self._set_position = None - self.schedule_update_ha_state() - _LOGGER.debug('cover process %s: %s', self.entity_id, { - 'position': self._position, - 'set_position': self._set_position, - 'requested_closing': self._requested_closing, - }) - - -class MrBondAirerProEntity(MiioCoverEntity): - def __init__(self, config): - name = config[CONF_NAME] - host = config[CONF_HOST] - token = config[CONF_TOKEN] - _LOGGER.info('Initializing with host %s (token %s...)', host, token[:5]) - - self._device = MiioDevice(host, token) - super().__init__(name, device=self._device, config=config) - self._motor_reverse = False - self._supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP - self._props = ['dry', 'led', 'motor', 'drytime', 'airer_location'] - self._vars.update({ - 'motor_open': 1, - 'motor_close': 2, - }) - self._subs = {} - - async def async_added_to_hass(self): - await super().async_added_to_hass() - self._motor_reverse = self.custom_config_bool('motor_reverse', False) - if self._motor_reverse: - self._vars.update({ - 'motor_open': 2, - 'motor_close': 1, - }) - - def get_single_prop(self, prop): - rls = self._device.get_properties([prop]) or [None] - return rls[0] - - async def async_get_single_prop(self, prop): - return await self.hass.async_add_executor_job(partial(self.get_single_prop, prop)) - - async def async_update(self): - attrs = [] - try: - attrs = await self.hass.async_add_executor_job( - partial(self._device.send, 'get_prop', self._props, extra_parameters={ - 'id': int(time.time() % 86400 * 1000), - }) - ) - self._available = True - except DeviceException as ex: - err = '%s' % ex - if err.find('-10000') > 0: - # Unknown Error: {'code': -10000, 'message': 'error'} - try: - attrs = [ - await self.async_get_single_prop('dry'), - await self.async_get_single_prop('led'), - await self.async_get_single_prop('motor'), - None, - None, - ] - self._available = True - except DeviceException as exc: - if self._available: - self._available = False - _LOGGER.error( - 'Got exception while fetching the state for %s (%s): %s %s', - self.entity_id, self._props, ex, exc - ) - else: - _LOGGER.error( - 'Got exception while fetching the state for %s (%s): %s', - self.entity_id, self._props, ex - ) - if self._available: - attrs = dict(zip(self._props, attrs)) - _LOGGER.debug('Got new state from %s: %s', self.entity_id, attrs) - self._state_attrs.update(attrs) - self._is_opening = int(attrs.get('motor', 0)) == 1 - self._is_closing = int(attrs.get('motor', 0)) == 2 - self._position = None - loc = attrs.get('airer_location', None) - if loc is None: - if self._is_opening: - self._position = 100 - if self._is_closing: - self._position = 0 - else: - if loc == 1: - self._position = 100 - if loc == 2: - self._position = 0 - self._state_attrs.update({ - 'position': self._position, - 'closed': self.is_closed, - 'stopped': bool(not self._is_opening and not self._is_closing), - }) - - add_lights = self._add_entities.get('light') - if 'light' in self._subs: - self._subs['light'].update_from_parent() - elif add_lights and 'led' in attrs: - self._subs['light'] = MrBondAirerProLightEntity(self) - add_lights([self._subs['light']], update_before_add=True) - - add_fans = self._add_entities.get('fan') - if 'fan' in self._subs: - self._subs['fan'].update_from_parent() - elif add_fans and 'dry' in attrs: - self._subs['fan'] = MrBondAirerProDryEntity(self, option={'keys': ['drytime']}) - add_fans([self._subs['fan']], update_before_add=True) - - def set_motor(self, val): - ret = self.send_miio_command('set_motor', [val]) - if ret: - self.update_attrs({'motor': val}) - self._is_opening = val == self._vars['motor_open'] - self._is_closing = val == self._vars['motor_close'] - if self._is_opening: - self._position = 100 - if self._is_closing: - self._position = 0 - return ret - - def open_cover(self, **kwargs): - return self.set_motor(self._vars['motor_open']) - - def close_cover(self, **kwargs): - return self.set_motor(self._vars['motor_close']) - - def stop_cover(self, **kwargs): - return self.set_motor(0) - - def set_led(self, val): - ret = self.send_miio_command('set_led', [val]) - if ret: - self.update_attrs({'led': val}) - return ret - - def set_dry(self, lvl): - if lvl == 0: - ret = self.send_miio_command('set_dryswitch', [0]) - elif lvl >= 4: - ret = self.send_miio_command('set_dryswitch', [1]) - else: - ret = self.send_miio_command('set_dry', [lvl]) - if ret: - self.update_attrs({'dry': lvl}) - return ret - - @property - def icon(self): - return 'mdi:hanger' - - -class MrBondAirerProLightEntity(LightSubEntity): - def __init__(self, parent: MrBondAirerProEntity, attr='led', option=None): - super().__init__(parent, attr, option) - - def update(self, data=None): - super().update(data) - if self._available: - attrs = self._state_attrs - self._state = int(attrs.get(self._attr, 0)) >= 1 - - def turn_on(self, **kwargs): - return self.call_parent('set_led', 1) - - def turn_off(self, **kwargs): - return self.call_parent('set_led', 0) - - -class MrBondAirerProDryEntity(FanSubEntity): - def __init__(self, parent: MrBondAirerProEntity, attr='dry', option=None): - super().__init__(parent, attr, option) - self._supported_features = FanEntityFeature.PRESET_MODE - - def update(self, data=None): - super().update() - if self._available: - attrs = self._state_attrs - self._state = int(attrs.get(self._attr, 0)) >= 1 - - def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): - return self.set_speed(speed or MrBondAirerProDryLevels(1).name) - - def turn_off(self, **kwargs): - return self.set_speed(MrBondAirerProDryLevels(0).name) - - @property - def speed(self): - return self.preset_mode - - @property - def speed_list(self): - return self.preset_modes - - def set_speed(self, speed: str): - return self.set_preset_mode(speed) - - @property - def preset_mode(self): - return MrBondAirerProDryLevels(int(self._state_attrs.get(self._attr, 0))).name - - @property - def preset_modes(self): - return [v.name for v in MrBondAirerProDryLevels] - - def set_preset_mode(self, speed: str): - lvl = MrBondAirerProDryLevels[speed].value - return self.call_parent('set_dry', lvl) - - -class MrBondAirerProDryLevels(Enum): - Off = 0 - Dry30Minutes = 1 - Dry60Minutes = 2 - Dry90Minutes = 3 - Dry120Minutes = 4 diff --git a/custom_components/xiaomi_miot/light.py b/custom_components/xiaomi_miot/light.py index 09cefb981..628c12855 100644 --- a/custom_components/xiaomi_miot/light.py +++ b/custom_components/xiaomi_miot/light.py @@ -51,9 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= model = str(config.get(CONF_MODEL) or '') spec = hass.data[DOMAIN]['miot_specs'].get(model) entities = [] - if model in ['mrbond.airer.m1s', 'mrbond.airer.m1pro']: - pass - elif isinstance(spec, MiotSpec): + if isinstance(spec, MiotSpec): for srv in spec.get_services(ENTITY_DOMAIN, 'ir_light_control', 'light_bath_heater'): if srv.name in ['ir_light_control']: entities.append(MiirLightEntity(config, srv))