From e36f27d6fd5b0655375bb90c66cbe108fe967269 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 24 Mar 2018 23:04:43 +0100 Subject: [PATCH] Xiaomi MiIO Fan: Xiaomi Air Humidifier integration (#12627) * Device support for the Xiaomi Air Humidifier. * Requirements updated. * "continuation line under-indented for visual indent" fixed. * Make hound happy. * Inadvertently added light.xiaomi_miio component removed from PR. * Service descriptions added. * One of the pylint errors fixed. * Redundancy removed. * pylint: disable=no-self-use added. The method signature is important here. * Pylint fixed. * Use a unique data key per domain. * Review incorporated. * Map of available attributes added. * Pylint fixed. Attribute "volume" added. * Don't use the support flag bit mask as model identifier. Determine support features and attributes at the constructor. Use starred expressions at dicts instead of copies. * Blank line removed. * Use Async / await syntax. * Make hound happy. * Xiaomi Air Humidifier CA support added. * Duplicate method removed. * Air Purifier V3 support added. * Don't abuse the system property supported_features anymore. * python-miio version bumped. * Clean-up. * Additional supported features refactoring completed. * Additional supported features renamed properly. * Unique id added. * Device unavailable handling improved. * Refactoring. * Missed const updated. * Incomplete Air Humidifier CA support fixed. * Review incorporated * The Air Humidifier CA supports the operation mode "auto" - the standard version doesn't * Attributes are part of the common set already * Revert "Attributes are part of the common set already" This reverts commit 40b443eba0e2fc55075479fd540f977fbf4b704a. * Comment added * Service description of the set_dry_{on,off} service added * Typo fixed --- homeassistant/components/fan/services.yaml | 111 ++- homeassistant/components/fan/xiaomi_miio.py | 745 ++++++++++++++++---- 2 files changed, 693 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a306cf7767c758..a74f67b83fb332 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on: description: Turn the buzzer on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_buzzer_off: description: Turn the buzzer off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_on: description: Turn the led on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_off: description: Turn the led off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_on: description: Turn the child lock on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_off: description: Turn the child lock off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_favorite_level: description: Set the favorite level. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' level: description: Level, between 0 and 16. example: 1 @@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness: description: Set the led brightness. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' brightness: description: Brightness (0 = Bright, 1 = Dim, 2 = Off) example: 1 + +xiaomi_miio_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +xiaomi_miio_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +xiaomi_miio_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +xiaomi_miio_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 09df55200a26d7..a1cb0431381fc2 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -1,16 +1,16 @@ """ -Support for Xiaomi Mi Air Purifier 2. +Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.xiaomi_miio/ """ import asyncio +from enum import Enum from functools import partial import logging import voluptuous as vol -from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, DOMAIN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, @@ -20,17 +20,40 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Xiaomi Air Purifier' -PLATFORM = 'xiaomi_miio' +DEFAULT_NAME = 'Xiaomi Miio Device' +DATA_KEY = 'fan.xiaomi_miio' + +CONF_MODEL = 'model' +MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' +MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3' +MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' +MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODEL): vol.In( + ['zhimi.airpurifier.m1', + 'zhimi.airpurifier.m2', + 'zhimi.airpurifier.ma1', + 'zhimi.airpurifier.ma2', + 'zhimi.airpurifier.sa1', + 'zhimi.airpurifier.sa2', + 'zhimi.airpurifier.v1', + 'zhimi.airpurifier.v2', + 'zhimi.airpurifier.v3', + 'zhimi.airpurifier.v5', + 'zhimi.airpurifier.v6', + 'zhimi.humidifier.v1', + 'zhimi.humidifier.ca1']), }) REQUIREMENTS = ['python-miio==0.3.8'] +ATTR_MODEL = 'model' + +# Air Purifier ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' ATTR_AIR_QUALITY_INDEX = 'aqi' @@ -45,20 +68,190 @@ ATTR_MOTOR_SPEED = 'motor_speed' ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' ATTR_PURIFY_VOLUME = 'purify_volume' - ATTR_BRIGHTNESS = 'brightness' ATTR_LEVEL = 'level' +ATTR_MOTOR2_SPEED = 'motor2_speed' +ATTR_ILLUMINANCE = 'illuminance' +ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id' +ATTR_FILTER_RFID_TAG = 'filter_rfid_tag' +ATTR_FILTER_TYPE = 'filter_type' +ATTR_LEARN_MODE = 'learn_mode' +ATTR_SLEEP_TIME = 'sleep_time' +ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count' +ATTR_EXTRA_FEATURES = 'extra_features' +ATTR_FEATURES = 'features' +ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported' +ATTR_AUTO_DETECT = 'auto_detect' +ATTR_SLEEP_MODE = 'sleep_mode' +ATTR_VOLUME = 'volume' +ATTR_USE_TIME = 'use_time' +ATTR_BUTTON_PRESSED = 'button_pressed' + +# Air Humidifier +ATTR_TARGET_HUMIDITY = 'target_humidity' +ATTR_TRANS_LEVEL = 'trans_level' +ATTR_HARDWARE_VERSION = 'hardware_version' + +# Air Humidifier CA +ATTR_SPEED = 'speed' +ATTR_DEPTH = 'depth' +ATTR_DRY = 'dry' + +# Map attributes to properties of the state object +AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_FAVORITE_LEVEL: 'favorite_level', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_LED: 'led', + ATTR_MOTOR_SPEED: 'motor_speed', + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_BUZZER: 'buzzer', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_SLEEP_MODE: 'sleep_mode', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_VOLUME: 'volume', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { + # Common set isn't used here. It's a very basic version of the device. + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_LED: 'led', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_MOTOR_SPEED: 'motor_speed', + # perhaps supported but unconfirmed + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_VOLUME: 'volume', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_MODE: 'mode', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_TRANS_LEVEL: 'trans_level', + ATTR_TARGET_HUMIDITY: 'target_humidity', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_BUTTON_PRESSED: 'button_pressed', + ATTR_USE_TIME: 'use_time', + ATTR_HARDWARE_VERSION: 'hardware_version', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { + **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER, + ATTR_SPEED: 'speed', + ATTR_DEPTH: 'depth', + ATTR_DRY: 'dry', +} + +OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] +OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] +OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', + 'Medium', 'High', 'Strong'] SUCCESS = ['ok'] +FEATURE_SET_BUZZER = 1 +FEATURE_SET_LED = 2 +FEATURE_SET_CHILD_LOCK = 4 +FEATURE_SET_LED_BRIGHTNESS = 8 +FEATURE_SET_FAVORITE_LEVEL = 16 +FEATURE_SET_AUTO_DETECT = 32 +FEATURE_SET_LEARN_MODE = 64 +FEATURE_SET_VOLUME = 128 +FEATURE_RESET_FILTER = 256 +FEATURE_SET_EXTRA_FEATURES = 512 +FEATURE_SET_TARGET_HUMIDITY = 1024 +FEATURE_SET_DRY = 2048 + +FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK) + +FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_LEARN_MODE | + FEATURE_RESET_FILTER | + FEATURE_SET_EXTRA_FEATURES) + +FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK | + FEATURE_SET_LED | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_AUTO_DETECT | + FEATURE_SET_VOLUME) + +FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED) + +FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_TARGET_HUMIDITY) + +FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER | + FEATURE_SET_DRY) + SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' -SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness' +SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' +SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on' +SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off' +SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on' +SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off' +SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume' +SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter' +SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features' +SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity' +SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on' +SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off' AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -74,6 +267,21 @@ vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16)) }) +SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_VOLUME): + vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) +}) + +SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FEATURES): + vol.All(vol.Coerce(int), vol.Range(min=0)) +}) + +SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_HUMIDITY): + vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80])) +}) + SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, @@ -81,59 +289,99 @@ SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, - SERVICE_SET_FAVORITE_LEVEL: { - 'method': 'async_set_favorite_level', - 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'}, + SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'}, + SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'}, + SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'}, + SERVICE_RESET_FILTER: {'method': 'async_reset_filter'}, SERVICE_SET_LED_BRIGHTNESS: { 'method': 'async_set_led_brightness', 'schema': SERVICE_SCHEMA_LED_BRIGHTNESS}, + SERVICE_SET_FAVORITE_LEVEL: { + 'method': 'async_set_favorite_level', + 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_VOLUME: { + 'method': 'async_set_volume', + 'schema': SERVICE_SCHEMA_VOLUME}, + SERVICE_SET_EXTRA_FEATURES: { + 'method': 'async_set_extra_features', + 'schema': SERVICE_SCHEMA_EXTRA_FEATURES}, + SERVICE_SET_TARGET_HUMIDITY: { + 'method': 'async_set_target_humidity', + 'schema': SERVICE_SCHEMA_TARGET_HUMIDITY}, + SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'}, + SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the air purifier from config.""" - from miio import AirPurifier, DeviceException - if PLATFORM not in hass.data: - hass.data[PLATFORM] = {} +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the miio fan device from config.""" + from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) token = config.get(CONF_TOKEN) + model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + unique_id = None - try: + if model is None: + try: + miio_device = Device(host, token) + device_info = miio_device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + except DeviceException: + raise PlatformNotReady + + if model.startswith('zhimi.airpurifier.'): + from miio import AirPurifier air_purifier = AirPurifier(host, token) - - xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier) - hass.data[PLATFORM][host] = xiaomi_air_purifier - except DeviceException: - raise PlatformNotReady - - async_add_devices([xiaomi_air_purifier], update_before_add=True) - - @asyncio.coroutine - def async_service_handler(service): + device = XiaomiAirPurifier(name, air_purifier, model, unique_id) + elif model.startswith('zhimi.humidifier.'): + from miio import AirHumidifier + air_humidifier = AirHumidifier(host, token) + device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) + else: + _LOGGER.error( + 'Unsupported device found! Please create an issue at ' + 'https://github.com/syssi/xiaomi_airpurifier/issues ' + 'and provide the following data: %s', model) + return False + + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) + + async def async_service_handler(service): """Map services to methods on XiaomiAirPurifier.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - devices = [device for device in hass.data[PLATFORM].values() if + devices = [device for device in hass.data[DATA_KEY].values() if device.entity_id in entity_ids] else: - devices = hass.data[PLATFORM].values() + devices = hass.data[DATA_KEY].values() update_tasks = [] for device in devices: - yield from getattr(device, method['method'])(**params) + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) update_tasks.append(device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for air_purifier_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[air_purifier_service].get( @@ -142,31 +390,22 @@ def async_service_handler(service): DOMAIN, air_purifier_service, async_service_handler, schema=schema) -class XiaomiAirPurifier(FanEntity): - """Representation of a Xiaomi Air Purifier.""" +class XiaomiGenericDevice(FanEntity): + """Representation of a generic Xiaomi device.""" - def __init__(self, name, air_purifier): - """Initialize the air purifier.""" + def __init__(self, name, device, model, unique_id): + """Initialize the generic Xiaomi device.""" self._name = name + self._device = device + self._model = model + self._unique_id = unique_id - self._air_purifier = air_purifier + self._available = False self._state = None self._state_attrs = { - ATTR_AIR_QUALITY_INDEX: None, - ATTR_TEMPERATURE: None, - ATTR_HUMIDITY: None, - ATTR_MODE: None, - ATTR_FILTER_HOURS_USED: None, - ATTR_FILTER_LIFE: None, - ATTR_FAVORITE_LEVEL: None, - ATTR_BUZZER: None, - ATTR_CHILD_LOCK: None, - ATTR_LED: None, - ATTR_LED_BRIGHTNESS: None, - ATTR_MOTOR_SPEED: None, - ATTR_AVERAGE_AIR_QUALITY_INDEX: None, - ATTR_PURIFY_VOLUME: None, + ATTR_MODEL: self._model, } + self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @property @@ -176,9 +415,14 @@ def supported_features(self): @property def should_poll(self): - """Poll the fan.""" + """Poll the device.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -187,7 +431,7 @@ def name(self): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -196,50 +440,116 @@ def device_state_attributes(self): @property def is_on(self): - """Return true if fan is on.""" + """Return true if device is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): - """Call an air purifier command handling error messages.""" + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a miio device command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) - _LOGGER.debug("Response received from air purifier: %s", result) + _LOGGER.debug("Response received from miio device: %s", result) return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: - """Turn the fan on.""" + async def async_turn_on(self, speed: str = None, + **kwargs) -> None: + """Turn the device on.""" if speed: # If operation mode was set the device must not be turned on. - result = yield from self.async_set_speed(speed) + result = await self.async_set_speed(speed) else: - result = yield from self._try_command( - "Turning the air purifier on failed.", self._air_purifier.on) + result = await self._try_command( + "Turning the miio device on failed.", self._device.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self: ToggleEntity, **kwargs) -> None: - """Turn the fan off.""" - result = yield from self._try_command( - "Turning the air purifier off failed.", self._air_purifier.off) + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + result = await self._try_command( + "Turning the miio device off failed.", self._device.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_set_buzzer_on(self): + """Turn the buzzer on.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device on failed.", + self._device.set_buzzer, True) + + async def async_set_buzzer_off(self): + """Turn the buzzer off.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device off failed.", + self._device.set_buzzer, False) + + async def async_set_child_lock_on(self): + """Turn the child lock on.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device on failed.", + self._device.set_child_lock, True) + + async def async_set_child_lock_off(self): + """Turn the child lock off.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device off failed.", + self._device.set_child_lock, False) + + +class XiaomiAirPurifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Purifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRPURIFIER_PRO: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO + self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO + elif self._model == MODEL_AIRPURIFIER_V3: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 + self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 + else: + self._device_features = FEATURE_FLAGS_AIRPURIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER + self._speed_list = OPERATION_MODES_AIRPURIFIER + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -249,40 +559,24 @@ def async_update(self): return try: - state = yield from self.hass.async_add_job( - self._air_purifier.status) + state = await self.hass.async_add_job( + self._device.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on - self._state_attrs = { - ATTR_TEMPERATURE: state.temperature, - ATTR_HUMIDITY: state.humidity, - ATTR_AIR_QUALITY_INDEX: state.aqi, - ATTR_MODE: state.mode.value, - ATTR_FILTER_HOURS_USED: state.filter_hours_used, - ATTR_FILTER_LIFE: state.filter_life_remaining, - ATTR_FAVORITE_LEVEL: state.favorite_level, - ATTR_BUZZER: state.buzzer, - ATTR_CHILD_LOCK: state.child_lock, - ATTR_LED: state.led, - ATTR_MOTOR_SPEED: state.motor_speed, - ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi, - ATTR_PURIFY_VOLUME: state.purify_volume, - } - - if state.led_brightness: - self._state_attrs[ - ATTR_LED_BRIGHTNESS] = state.led_brightness.value + self._state_attrs.update( + {key: self._extract_value_from_attribute(state, value) for + key, value in self._available_attributes.items()}) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" - from miio.airpurifier import OperationMode - return [mode.name for mode in OperationMode] + return self._speed_list @property def speed(self): @@ -294,70 +588,227 @@ def speed(self): return None - @asyncio.coroutine - def async_set_speed(self: ToggleEntity, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - _LOGGER.debug("Setting the operation mode to: %s", speed) - from miio.airpurifier import OperationMode + if self.supported_features & SUPPORT_SET_SPEED == 0: + return - yield from self._try_command( - "Setting operation mode of the air purifier failed.", - self._air_purifier.set_mode, OperationMode[speed.title()]) + from miio.airpurifier import OperationMode - @asyncio.coroutine - def async_set_buzzer_on(self): - """Turn the buzzer on.""" - yield from self._try_command( - "Turning the buzzer of the air purifier on failed.", - self._air_purifier.set_buzzer, True) + _LOGGER.debug("Setting the operation mode to: %s", speed) - @asyncio.coroutine - def async_set_buzzer_off(self): - """Turn the buzzer off.""" - yield from self._try_command( - "Turning the buzzer of the air purifier off failed.", - self._air_purifier.set_buzzer, False) + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) - @asyncio.coroutine - def async_set_led_on(self): + async def async_set_led_on(self): """Turn the led on.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, True) + if self._device_features & FEATURE_SET_LED == 0: + return + + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, True) - @asyncio.coroutine - def async_set_led_off(self): + async def async_set_led_off(self): """Turn the led off.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, False) + if self._device_features & FEATURE_SET_LED == 0: + return - @asyncio.coroutine - def async_set_child_lock_on(self): - """Turn the child lock on.""" - yield from self._try_command( - "Turning the child lock of the air purifier on failed.", - self._air_purifier.set_child_lock, True) + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, False) - @asyncio.coroutine - def async_set_child_lock_off(self): - """Turn the child lock off.""" - yield from self._try_command( - "Turning the child lock of the air purifier off failed.", - self._air_purifier.set_child_lock, False) - - @asyncio.coroutine - def async_set_led_brightness(self, brightness: int = 2): + async def async_set_led_brightness(self, brightness: int = 2): """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + from miio.airpurifier import LedBrightness - yield from self._try_command( - "Setting the led brightness of the air purifier failed.", - self._air_purifier.set_led_brightness, LedBrightness(brightness)) + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) - @asyncio.coroutine - def async_set_favorite_level(self, level: int = 1): + async def async_set_favorite_level(self, level: int = 1): """Set the favorite level.""" - yield from self._try_command( - "Setting the favorite level of the air purifier failed.", - self._air_purifier.set_favorite_level, level) + if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: + return + + await self._try_command( + "Setting the favorite level of the miio device failed.", + self._device.set_favorite_level, level) + + async def async_set_auto_detect_on(self): + """Turn the auto detect on.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device on failed.", + self._device.set_auto_detect, True) + + async def async_set_auto_detect_off(self): + """Turn the auto detect off.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device off failed.", + self._device.set_auto_detect, False) + + async def async_set_learn_mode_on(self): + """Turn the learn mode on.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device on failed.", + self._device.set_learn_mode, True) + + async def async_set_learn_mode_off(self): + """Turn the learn mode off.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device off failed.", + self._device.set_learn_mode, False) + + async def async_set_volume(self, volume: int = 50): + """Set the sound volume.""" + if self._device_features & FEATURE_SET_VOLUME == 0: + return + + await self._try_command( + "Setting the sound volume of the miio device failed.", + self._device.set_volume, volume) + + async def async_set_extra_features(self, features: int = 1): + """Set the extra features.""" + if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: + return + + await self._try_command( + "Setting the extra features of the miio device failed.", + self._device.set_extra_features, features) + + async def async_reset_filter(self): + """Reset the filter lifetime and usage.""" + if self._device_features & FEATURE_RESET_FILTER == 0: + return + + await self._try_command( + "Resetting the filter lifetime of the miio device failed.", + self._device.reset_filter) + + +class XiaomiAirHumidifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Humidifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + from miio.airpurifier import OperationMode + + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRHUMIDIFIER_CA: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + self._speed_list = [mode.name for mode in OperationMode] + else: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER + self._speed_list = [mode.name for mode in OperationMode if + mode.name != 'Auto'] + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + + # 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_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()}) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def speed(self): + """Return the current speed.""" + if self._state: + from miio.airhumidifier import OperationMode + + return OperationMode(self._state_attrs[ATTR_MODE]).name + + return None + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self.supported_features & SUPPORT_SET_SPEED == 0: + return + + from miio.airhumidifier import OperationMode + + _LOGGER.debug("Setting the operation mode to: %s", speed) + + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) + + async def async_set_led_brightness(self, brightness: int = 2): + """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + + from miio.airhumidifier import LedBrightness + + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) + + async def async_set_target_humidity(self, humidity: int = 40): + """Set the target humidity.""" + if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0: + return + + await self._try_command( + "Setting the target humidity of the miio device failed.", + self._device.set_target_humidity, humidity) + + async def async_set_dry_on(self): + """Turn the dry mode on.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, True) + + async def async_set_dry_off(self): + """Turn the dry mode off.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, False)