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
This commit is contained in:
Sebastian Muszynski 2018-03-24 23:04:43 +01:00 committed by Teemu R
parent 11930d5f20
commit e36f27d6fd
2 changed files with 691 additions and 161 deletions

View File

@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
description: Turn the buzzer on. description: Turn the buzzer on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_buzzer_off: xiaomi_miio_set_buzzer_off:
description: Turn the buzzer off. description: Turn the buzzer off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_on: xiaomi_miio_set_led_on:
description: Turn the led on. description: Turn the led on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_off: xiaomi_miio_set_led_off:
description: Turn the led off. description: Turn the led off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_on: xiaomi_miio_set_child_lock_on:
description: Turn the child lock on. description: Turn the child lock on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_off: xiaomi_miio_set_child_lock_off:
description: Turn the child lock off. description: Turn the child lock off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_favorite_level: xiaomi_miio_set_favorite_level:
description: Set the favorite level. description: Set the favorite level.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
level: level:
description: Level, between 0 and 16. description: Level, between 0 and 16.
example: 1 example: 1
@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
description: Set the led brightness. description: Set the led brightness.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
brightness: brightness:
description: Brightness (0 = Bright, 1 = Dim, 2 = Off) description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
example: 1 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'

View File

@ -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 For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.xiaomi_miio/ https://home-assistant.io/components/fan.xiaomi_miio/
""" """
import asyncio import asyncio
from enum import Enum
from functools import partial from functools import partial
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN, ) SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Air Purifier' DEFAULT_NAME = 'Xiaomi Miio Device'
PLATFORM = 'xiaomi_miio' 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), 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_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'] REQUIREMENTS = ['python-miio==0.3.8']
ATTR_MODEL = 'model'
# Air Purifier
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'
ATTR_AIR_QUALITY_INDEX = 'aqi' ATTR_AIR_QUALITY_INDEX = 'aqi'
@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
ATTR_MOTOR_SPEED = 'motor_speed' ATTR_MOTOR_SPEED = 'motor_speed'
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
ATTR_PURIFY_VOLUME = 'purify_volume' ATTR_PURIFY_VOLUME = 'purify_volume'
ATTR_BRIGHTNESS = 'brightness' ATTR_BRIGHTNESS = 'brightness'
ATTR_LEVEL = 'level' 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'] 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_ON = 'xiaomi_miio_set_buzzer_on'
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' 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_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({ AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16)) 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_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
SERVICE_SET_FAVORITE_LEVEL: { SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
'method': 'async_set_favorite_level', SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, 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: { SERVICE_SET_LED_BRIGHTNESS: {
'method': 'async_set_led_brightness', 'method': 'async_set_led_brightness',
'schema': SERVICE_SCHEMA_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 # pylint: disable=unused-argument
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the air purifier from config.""" """Set up the miio fan device from config."""
from miio import AirPurifier, DeviceException from miio import Device, DeviceException
if PLATFORM not in hass.data: if DATA_KEY not in hass.data:
hass.data[PLATFORM] = {} hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN) token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
unique_id = None
if model is None:
try: try:
air_purifier = AirPurifier(host, token) miio_device = Device(host, token)
device_info = miio_device.info()
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier) model = device_info.model
hass.data[PLATFORM][host] = xiaomi_air_purifier 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: except DeviceException:
raise PlatformNotReady raise PlatformNotReady
async_add_devices([xiaomi_air_purifier], update_before_add=True) if model.startswith('zhimi.airpurifier.'):
from miio import AirPurifier
air_purifier = AirPurifier(host, token)
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
@asyncio.coroutine hass.data[DATA_KEY][host] = device
def async_service_handler(service): async_add_devices([device], update_before_add=True)
async def async_service_handler(service):
"""Map services to methods on XiaomiAirPurifier.""" """Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD.get(service.service) method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items() params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID} if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids: 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] device.entity_id in entity_ids]
else: else:
devices = hass.data[PLATFORM].values() devices = hass.data[DATA_KEY].values()
update_tasks = [] update_tasks = []
for device in devices: 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)) update_tasks.append(device.async_update_ha_state(True))
if update_tasks: 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: for air_purifier_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[air_purifier_service].get( schema = SERVICE_TO_METHOD[air_purifier_service].get(
@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DOMAIN, air_purifier_service, async_service_handler, schema=schema) DOMAIN, air_purifier_service, async_service_handler, schema=schema)
class XiaomiAirPurifier(FanEntity): class XiaomiGenericDevice(FanEntity):
"""Representation of a Xiaomi Air Purifier.""" """Representation of a generic Xiaomi device."""
def __init__(self, name, air_purifier): def __init__(self, name, device, model, unique_id):
"""Initialize the air purifier.""" """Initialize the generic Xiaomi device."""
self._name = name 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 = None
self._state_attrs = { self._state_attrs = {
ATTR_AIR_QUALITY_INDEX: None, ATTR_MODEL: self._model,
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,
} }
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False self._skip_update = False
@property @property
@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def should_poll(self): def should_poll(self):
"""Poll the fan.""" """Poll the device."""
return True return True
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def available(self): def available(self):
"""Return true when state is known.""" """Return true when state is known."""
return self._state is not None return self._available
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def is_on(self): def is_on(self):
"""Return true if fan is on.""" """Return true if device is on."""
return self._state return self._state
@asyncio.coroutine @staticmethod
def _try_command(self, mask_error, func, *args, **kwargs): def _extract_value_from_attribute(state, attribute):
"""Call an air purifier command handling error messages.""" 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 from miio import DeviceException
try: try:
result = yield from self.hass.async_add_job( result = await self.hass.async_add_job(
partial(func, *args, **kwargs)) 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 return result == SUCCESS
except DeviceException as exc: except DeviceException as exc:
_LOGGER.error(mask_error, exc) _LOGGER.error(mask_error, exc)
self._available = False
return False return False
@asyncio.coroutine async def async_turn_on(self, speed: str = None,
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: **kwargs) -> None:
"""Turn the fan on.""" """Turn the device on."""
if speed: if speed:
# If operation mode was set the device must not be turned on. # 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: else:
result = yield from self._try_command( result = await self._try_command(
"Turning the air purifier on failed.", self._air_purifier.on) "Turning the miio device on failed.", self._device.on)
if result: if result:
self._state = True self._state = True
self._skip_update = True self._skip_update = True
@asyncio.coroutine async def async_turn_off(self, **kwargs) -> None:
def async_turn_off(self: ToggleEntity, **kwargs) -> None: """Turn the device off."""
"""Turn the fan off.""" result = await self._try_command(
result = yield from self._try_command( "Turning the miio device off failed.", self._device.off)
"Turning the air purifier off failed.", self._air_purifier.off)
if result: if result:
self._state = False self._state = False
self._skip_update = True self._skip_update = True
@asyncio.coroutine async def async_set_buzzer_on(self):
def async_update(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.""" """Fetch state from the device."""
from miio import DeviceException from miio import DeviceException
@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
return return
try: try:
state = yield from self.hass.async_add_job( state = await self.hass.async_add_job(
self._air_purifier.status) self._device.status)
_LOGGER.debug("Got new state: %s", state) _LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on self._state = state.is_on
self._state_attrs = { self._state_attrs.update(
ATTR_TEMPERATURE: state.temperature, {key: self._extract_value_from_attribute(state, value) for
ATTR_HUMIDITY: state.humidity, key, value in self._available_attributes.items()})
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
except DeviceException as ex: except DeviceException as ex:
self._state = None self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)
@property @property
def speed_list(self: ToggleEntity) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
from miio.airpurifier import OperationMode return self._speed_list
return [mode.name for mode in OperationMode]
@property @property
def speed(self): def speed(self):
@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
return None return None
@asyncio.coroutine async def async_set_speed(self, speed: str) -> None:
def async_set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
_LOGGER.debug("Setting the operation mode to: %s", speed) if self.supported_features & SUPPORT_SET_SPEED == 0:
return
from miio.airpurifier import OperationMode from miio.airpurifier import OperationMode
yield from self._try_command( _LOGGER.debug("Setting the operation mode to: %s", speed)
"Setting operation mode of the air purifier failed.",
self._air_purifier.set_mode, OperationMode[speed.title()])
@asyncio.coroutine await self._try_command(
def async_set_buzzer_on(self): "Setting operation mode of the miio device failed.",
"""Turn the buzzer on.""" self._device.set_mode, OperationMode[speed.title()])
yield from self._try_command(
"Turning the buzzer of the air purifier on failed.",
self._air_purifier.set_buzzer, True)
@asyncio.coroutine async def async_set_led_on(self):
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)
@asyncio.coroutine
def async_set_led_on(self):
"""Turn the led on.""" """Turn the led on."""
yield from self._try_command( if self._device_features & FEATURE_SET_LED == 0:
"Turning the led of the air purifier off failed.", return
self._air_purifier.set_led, True)
@asyncio.coroutine await self._try_command(
def async_set_led_off(self): "Turning the led of the miio device off failed.",
self._device.set_led, True)
async def async_set_led_off(self):
"""Turn the led off.""" """Turn the led off."""
yield from self._try_command( if self._device_features & FEATURE_SET_LED == 0:
"Turning the led of the air purifier off failed.", return
self._air_purifier.set_led, False)
@asyncio.coroutine await self._try_command(
def async_set_child_lock_on(self): "Turning the led of the miio device off failed.",
"""Turn the child lock on.""" self._device.set_led, False)
yield from self._try_command(
"Turning the child lock of the air purifier on failed.",
self._air_purifier.set_child_lock, True)
@asyncio.coroutine async def async_set_led_brightness(self, brightness: int = 2):
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):
"""Set the led brightness.""" """Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airpurifier import LedBrightness from miio.airpurifier import LedBrightness
yield from self._try_command( await self._try_command(
"Setting the led brightness of the air purifier failed.", "Setting the led brightness of the miio device failed.",
self._air_purifier.set_led_brightness, LedBrightness(brightness)) self._device.set_led_brightness, LedBrightness(brightness))
@asyncio.coroutine async def async_set_favorite_level(self, level: int = 1):
def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level.""" """Set the favorite level."""
yield from self._try_command( if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
"Setting the favorite level of the air purifier failed.", return
self._air_purifier.set_favorite_level, level)
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)