Skip to content

Commit

Permalink
👕 add miio2miot for mrbond.airer.m1pro/m1s
Browse files Browse the repository at this point in the history
  • Loading branch information
al-one committed Sep 9, 2024
1 parent 8112100 commit 64cc5e7
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 282 deletions.
7 changes: 7 additions & 0 deletions custom_components/xiaomi_miot/core/device_customizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion custom_components/xiaomi_miot/core/miio2miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions custom_components/xiaomi_miot/core/miio2miot_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
279 changes: 1 addition & 278 deletions custom_components/xiaomi_miot/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
4 changes: 1 addition & 3 deletions custom_components/xiaomi_miot/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 64cc5e7

Please sign in to comment.