Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AirDogX3, AirDogX5, AirDogX7SM support #159

Merged
merged 12 commits into from
May 6, 2021
254 changes: 254 additions & 0 deletions custom_components/xiaomi_miio_airpurifier/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
Fan,
FanLeshow,
FanP5,
AirDogX3,
AirDogX5,
AirDogX7SM,
FanC1,
FanP9,
FanP10,
Expand Down Expand Up @@ -65,6 +68,9 @@
from miio.fan_leshow import ( # pylint: disable=import-error, import-error
OperationMode as FanLeshowOperationMode,
)
from miio.airpurifier_airdog import ( # pylint: disable=import-error, import-error
OperationMode as AirDogOperationMode,
)
from miio.fan_miot import (
OperationModeMiot as FanOperationModeMiot
)
Expand Down Expand Up @@ -116,6 +122,9 @@
MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2"
MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4"
MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
MODEL_AIRPURIFIER_AIRDOG_X3 = "airdog.airpurifier.x3"
MODEL_AIRPURIFIER_AIRDOG_X5 = "airdog.airpurifier.x5"
MODEL_AIRPURIFIER_AIRDOG_X7SM = "airdog.airpurifier.x7sm"

MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
Expand Down Expand Up @@ -167,6 +176,9 @@
MODEL_AIRPURIFIER_2H,
MODEL_AIRPURIFIER_3,
MODEL_AIRPURIFIER_3H,
MODEL_AIRPURIFIER_AIRDOG_X3,
MODEL_AIRPURIFIER_AIRDOG_X5,
MODEL_AIRPURIFIER_AIRDOG_X7SM,
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
Expand Down Expand Up @@ -296,6 +308,11 @@
PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H]
HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]

# AirDogX7SM
ATTR_FORMALDEHYDE = "hcho"
# AirDogX3, AirDogX5, AirDogX7SM
ATTR_CLEAN_FILTERS = "clean_filters"

# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_TEMPERATURE: "temperature",
Expand Down Expand Up @@ -587,6 +604,23 @@
ATTR_ERROR_DETECTED: "error_detected",
}

AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3 = {
ATTR_MODE: "mode",
ATTR_SPEED: "speed",
ATTR_CHILD_LOCK: "child_lock",
ATTR_CLEAN_FILTERS: "clean_filters",
ATTR_PM25: "pm25",
}

AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X5 = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3,
}

AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X7SM = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3,
ATTR_FORMALDEHYDE: "hcho",
}

AVAILABLE_ATTRIBUTES_FAN_1C = {
ATTR_MODE: "mode",
ATTR_RAW_SPEED: "speed",
Expand Down Expand Up @@ -830,6 +864,8 @@
FEATURE_FLAGS_FAN_LESHOW_SS4 = FEATURE_SET_BUZZER
FEATURE_FLAGS_FAN_1C = FEATURE_FLAGS_FAN

FEATURE_FLAGS_AIRPURIFIER_AIRDOG = FEATURE_SET_CHILD_LOCK

SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"
SERVICE_SET_FAN_LED_ON = "fan_set_led_on"
Expand All @@ -850,6 +886,7 @@
SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity"
SERVICE_SET_DRY_ON = "fan_set_dry_on"
SERVICE_SET_DRY_OFF = "fan_set_dry_off"
SERVICE_SET_FILTERS_CLEANED = "fan_set_filters_cleaned"

# Airhumidifer CA4
SERVICE_SET_CLEAN_MODE_ON = "fan_set_clean_mode_on"
Expand Down Expand Up @@ -1007,6 +1044,7 @@
SERVICE_SET_WET_PROTECTION_OFF: {"method": "async_set_wet_protection_off"},
SERVICE_SET_CLEAN_MODE_ON: {"method": "async_set_clean_mode_on"},
SERVICE_SET_CLEAN_MODE_OFF: {"method": "async_set_clean_mode_off"},
SERVICE_SET_FILTERS_CLEANED: {"method": "async_set_filters_cleaned"},
}


Expand Down Expand Up @@ -1095,6 +1133,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
elif model == MODEL_FAN_LESHOW_SS4:
fan = FanLeshow(host, token, model=model)
device = XiaomiFanLeshow(name, fan, model, unique_id, retries)
elif model == MODEL_AIRPURIFIER_AIRDOG_X3:
air_purifier = AirDogX3(host, token)
device = XiaomiAirDog(name, air_purifier, model, unique_id, retries)
elif model == MODEL_AIRPURIFIER_AIRDOG_X5:
air_purifier = AirDogX5(host, token)
device = XiaomiAirDog(name, air_purifier, model, unique_id, retries)
elif model == MODEL_AIRPURIFIER_AIRDOG_X7SM:
air_purifier = AirDogX7SM(host, token)
device = XiaomiAirDog(name, air_purifier, model, unique_id, retries)
elif model == MODEL_FAN_1C:
fan = FanC1(host, token, model=model)
device = XiaomiFan1C(name, fan, model, unique_id, retries)
Expand Down Expand Up @@ -2805,3 +2852,210 @@ async def async_set_natural_mode_off(self):
self._device.set_mode,
FanOperationModeMiot.Normal,
)


class XiaomiAirDog(XiaomiGenericDevice):
"""Representation of a Xiaomi AirDog air purifiers."""

def __init__(self, name, device, model, unique_id, retries=0):
"""Initialize the plug switch."""
super().__init__(name, device, model, unique_id, retries)

self._device_features = FEATURE_FLAGS_AIRPURIFIER_AIRDOG

if self._model == MODEL_AIRPURIFIER_AIRDOG_X7SM:
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X7SM
else:
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3

self._preset_modes_to_mode_speed = {
'Auto': (AirDogOperationMode('auto'), 1),
'Night mode': (AirDogOperationMode('sleep'), 1),
'Speed 1': (AirDogOperationMode('manual'), 1),
'Speed 2': (AirDogOperationMode('manual'), 2),
'Speed 3': (AirDogOperationMode('manual'), 3),
'Speed 4': (AirDogOperationMode('manual'), 4),
}
if self._model == MODEL_AIRPURIFIER_AIRDOG_X7SM:
self._preset_modes_to_mode_speed['Speed 5'] = (AirDogOperationMode('Manual'), 5)

self._mode_speed_to_preset_modes = {}
for key, value in self._preset_modes_to_mode_speed.items():
self._mode_speed_to_preset_modes[value] = key

self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}
)

async def async_update(self):
"""Fetch state from the device."""
# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return

try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)

self._available = True
self._state = state.is_on
self._state_attrs.update(
{
key: self._extract_value_from_attribute(state, value)
for key, value in self._available_attributes.items()
}
)

self._retry = 0

except DeviceException as ex:
self._retry = self._retry + 1
if self._retry < self._retries:
_LOGGER.info(
"Got exception while fetching the state: %s , _retry=%s",
ex,
self._retry,
)
else:
self._available = False
_LOGGER.error(
"Got exception while fetching the state: %s , _retry=%s",
ex,
self._retry,
)

@property
def preset_modes(self):
"""Get the list of available preset modes."""
return list(self._preset_modes_to_mode_speed.keys())

@property
def preset_mode(self):
"""Get the current preset mode."""
if self._state:
# There are invalid modes, such as 'Auto 2'. There are no presets for them
if (AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED]) in self._mode_speed_to_preset_modes:
return self._mode_speed_to_preset_modes[(AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED])]

return None

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
_LOGGER.debug("Setting the preset mode to: %s", preset_mode)
_LOGGER.debug("Calling set_mode_and_speed with parameters: %s", self._preset_modes_to_mode_speed[preset_mode])

# Following is true on AirDogX5 with firmware 1.3.5_0005. Maybe this is different for other models. Needs testing

# It looks like the device was not designed to switch from any arbitrary mode to any other mode.
# Some of the combinations produce unexpected results
#
# For example, switching from 'Auto' to 'Speed X' switches to Manual mode, but always sets speed to 1, regardless of the speed parameter.
#
# Switching from 'Night mode' to 'Speed X' sets device in Auto mode with speed X.
# Tihs 'Auto X' state is quite strange and does not seem to be useful.
# Furthermore, we request Manual mode and get Auto.
# Switching from 'Auto X' mode to 'Manual X' works just fine.
# Switching from 'Auto X' mode to 'Manual Y' switches to 'Manual X'.

# Here is a full table of device behaviour

# FROM TO RESULT
#'Night mode' ->
# 'Auto' Good
# 'Speed 1' 'Auto 1' + repeat -> Good
# 'Speed 2' 'Auto 2' + repeat -> Good
# 'Speed 3' 'Auto 3' + repeat -> Good
# 'Speed 4' 'Auto 4' + repeat -> Good
#'Speed 1'
# 'Night mode' Good
# 'Auto' Good
#'Speed 2' ->
# 'Night mode' Good
# 'Auto' Good
#'Speed 3' ->
# 'Night mode' Good
# 'Auto' Good
#'Speed 4' ->
# 'Night mode' Good
#'Auto'->
# 'Night mode' Good
# 'Speed 1' Good
# 'Speed 2' 'Speed 1' + repeat -> Good
# 'Speed 3' 'Speed 1' + repeat -> Good
# 'Speed 4' 'Speed 1' + repeat -> Good


# To allow switching from any mode to any other mode command is repeated twice when switching is from 'Night mode' or 'Auto' to 'Speed X'.

await self._try_command(
"Setting preset mode of the miio device failed.",
self._device.set_mode_and_speed,
*self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple
)

if self._state_attrs[ATTR_MODE] in ('auto', 'sleep') and self._preset_modes_to_mode_speed[preset_mode][0].value == 'manual':
await self._try_command(
"Setting preset mode of the miio device failed.",
self._device.set_mode_and_speed,
*self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple
)

self._state_attrs.update(
{
ATTR_MODE: self._preset_modes_to_mode_speed[preset_mode][0].value,
ATTR_SPEED: self._preset_modes_to_mode_speed[preset_mode][1],
}
)
self._skip_update = True

async def async_set_filters_cleaned(self):
"""Set filters cleaned."""
await self._try_command(
"Setting filters cleaned failed.",
self._device.set_filters_cleaned,
)



async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the device on."""
await super().async_turn_on(speed, percentage, preset_mode, **kwargs)

self._state = True
self._skip_update = True

async def async_turn_off(self, **kwargs) -> None:
"""Turn the device off."""
await super().async_turn_off(**kwargs)

self._state = False
self._skip_update = True


async def async_set_child_lock_on(self):
"""Turn the child lock on."""
await super().async_set_child_lock_on()
self._state_attrs.update(
{
ATTR_CHILD_LOCK: True,
}
)
self._skip_update = True


async def async_set_child_lock_off(self):
"""Turn the child lock off."""
await super().async_set_child_lock_off()
self._state_attrs.update(
{
ATTR_CHILD_LOCK: False,
}
)
self._skip_update = True
9 changes: 9 additions & 0 deletions custom_components/xiaomi_miio_airpurifier/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,12 @@ fan_set_dry_off:
entity_id:
description: Name of the xiaomi miio entity.
example: "xiaomi_miio_airpurifier.xiaomi_miio_device"

fan_set_filters_cleaned:
description: Inform the device that filters are cleaned.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: "xiaomi_miio_airpurifier.xiaomi_miio_device"