1026 lines
36 KiB
Python
1026 lines
36 KiB
Python
"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier."""
|
|
import asyncio
|
|
from enum import Enum
|
|
from functools import partial
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
|
SUPPORT_SET_SPEED, DOMAIN, )
|
|
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
|
ATTR_ENTITY_ID, )
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_NAME = 'Xiaomi Miio Device'
|
|
DATA_KEY = 'fan.xiaomi_miio'
|
|
|
|
CONF_MODEL = 'model'
|
|
MODEL_AIRPURIFIER_V1 = 'zhimi.airpurifier.v1'
|
|
MODEL_AIRPURIFIER_V2 = 'zhimi.airpurifier.v2'
|
|
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
|
|
MODEL_AIRPURIFIER_V5 = 'zhimi.airpurifier.v5'
|
|
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
|
|
MODEL_AIRPURIFIER_PRO_V7 = 'zhimi.airpurifier.v7'
|
|
MODEL_AIRPURIFIER_M1 = 'zhimi.airpurifier.m1'
|
|
MODEL_AIRPURIFIER_M2 = 'zhimi.airpurifier.m2'
|
|
MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1'
|
|
MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2'
|
|
MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1'
|
|
MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2'
|
|
MODEL_AIRPURIFIER_2S = 'zhimi.airpurifier.mc1'
|
|
|
|
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
|
|
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
|
|
|
|
MODEL_AIRFRESH_VA2 = 'zhimi.airfresh.va2'
|
|
|
|
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(
|
|
[MODEL_AIRPURIFIER_V1,
|
|
MODEL_AIRPURIFIER_V2,
|
|
MODEL_AIRPURIFIER_V3,
|
|
MODEL_AIRPURIFIER_V5,
|
|
MODEL_AIRPURIFIER_PRO,
|
|
MODEL_AIRPURIFIER_PRO_V7,
|
|
MODEL_AIRPURIFIER_M1,
|
|
MODEL_AIRPURIFIER_M2,
|
|
MODEL_AIRPURIFIER_MA1,
|
|
MODEL_AIRPURIFIER_MA2,
|
|
MODEL_AIRPURIFIER_SA1,
|
|
MODEL_AIRPURIFIER_SA2,
|
|
MODEL_AIRPURIFIER_2S,
|
|
MODEL_AIRHUMIDIFIER_V1,
|
|
MODEL_AIRHUMIDIFIER_CA,
|
|
MODEL_AIRFRESH_VA2,
|
|
]),
|
|
})
|
|
|
|
ATTR_MODEL = 'model'
|
|
|
|
# Air Purifier
|
|
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_BUZZER = 'buzzer'
|
|
ATTR_CHILD_LOCK = 'child_lock'
|
|
ATTR_LED = 'led'
|
|
ATTR_LED_BRIGHTNESS = 'led_brightness'
|
|
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_MOTOR_SPEED = 'motor_speed'
|
|
ATTR_DEPTH = 'depth'
|
|
ATTR_DRY = 'dry'
|
|
|
|
# Air Fresh
|
|
ATTR_CO2 = 'co2'
|
|
|
|
# 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_LEARN_MODE: 'learn_mode',
|
|
ATTR_EXTRA_FEATURES: 'extra_features',
|
|
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
|
|
ATTR_BUTTON_PRESSED: 'button_pressed',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
|
|
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
|
ATTR_PURIFY_VOLUME: 'purify_volume',
|
|
ATTR_SLEEP_TIME: 'sleep_time',
|
|
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
|
ATTR_AUTO_DETECT: 'auto_detect',
|
|
ATTR_USE_TIME: 'use_time',
|
|
ATTR_BUZZER: 'buzzer',
|
|
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
|
ATTR_SLEEP_MODE: 'sleep_mode',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
|
|
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
|
ATTR_PURIFY_VOLUME: 'purify_volume',
|
|
ATTR_USE_TIME: 'use_time',
|
|
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',
|
|
# perhaps supported but unconfirmed
|
|
ATTR_AUTO_DETECT: 'auto_detect',
|
|
ATTR_SLEEP_TIME: 'sleep_time',
|
|
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = {
|
|
**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_2S = {
|
|
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
|
ATTR_BUZZER: 'buzzer',
|
|
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
|
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
|
ATTR_FILTER_TYPE: 'filter_type',
|
|
ATTR_ILLUMINANCE: 'illuminance',
|
|
}
|
|
|
|
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_COMMON = {
|
|
ATTR_TEMPERATURE: 'temperature',
|
|
ATTR_HUMIDITY: 'humidity',
|
|
ATTR_MODE: 'mode',
|
|
ATTR_BUZZER: 'buzzer',
|
|
ATTR_CHILD_LOCK: 'child_lock',
|
|
ATTR_TARGET_HUMIDITY: 'target_humidity',
|
|
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
|
ATTR_USE_TIME: 'use_time',
|
|
ATTR_HARDWARE_VERSION: 'hardware_version',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
|
|
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
|
|
ATTR_TRANS_LEVEL: 'trans_level',
|
|
ATTR_BUTTON_PRESSED: 'button_pressed',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
|
|
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
|
|
ATTR_MOTOR_SPEED: 'speed',
|
|
ATTR_DEPTH: 'depth',
|
|
ATTR_DRY: 'dry',
|
|
}
|
|
|
|
AVAILABLE_ATTRIBUTES_AIRFRESH = {
|
|
ATTR_TEMPERATURE: 'temperature',
|
|
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
|
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
|
ATTR_CO2: 'co2',
|
|
ATTR_HUMIDITY: 'humidity',
|
|
ATTR_MODE: 'mode',
|
|
ATTR_LED: 'led',
|
|
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
|
ATTR_BUZZER: 'buzzer',
|
|
ATTR_CHILD_LOCK: 'child_lock',
|
|
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
|
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
|
ATTR_USE_TIME: 'use_time',
|
|
ATTR_MOTOR_SPEED: 'motor_speed',
|
|
ATTR_EXTRA_FEATURES: 'extra_features',
|
|
}
|
|
|
|
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
|
|
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
|
|
OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO
|
|
OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite']
|
|
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
|
|
'Medium', 'High', 'Strong']
|
|
OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low',
|
|
'Middle', '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_AIRPURIFIER = (FEATURE_SET_BUZZER |
|
|
FEATURE_SET_CHILD_LOCK |
|
|
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_PRO_V7 = (FEATURE_SET_CHILD_LOCK |
|
|
FEATURE_SET_LED |
|
|
FEATURE_SET_FAVORITE_LEVEL |
|
|
FEATURE_SET_VOLUME)
|
|
|
|
FEATURE_FLAGS_AIRPURIFIER_2S = (FEATURE_SET_BUZZER |
|
|
FEATURE_SET_CHILD_LOCK |
|
|
FEATURE_SET_LED |
|
|
FEATURE_SET_FAVORITE_LEVEL)
|
|
|
|
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER |
|
|
FEATURE_SET_CHILD_LOCK |
|
|
FEATURE_SET_LED)
|
|
|
|
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_SET_BUZZER |
|
|
FEATURE_SET_CHILD_LOCK |
|
|
FEATURE_SET_LED_BRIGHTNESS |
|
|
FEATURE_SET_TARGET_HUMIDITY)
|
|
|
|
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
|
|
FEATURE_SET_DRY)
|
|
|
|
FEATURE_FLAGS_AIRFRESH = (FEATURE_SET_BUZZER |
|
|
FEATURE_SET_CHILD_LOCK |
|
|
FEATURE_SET_LED |
|
|
FEATURE_SET_LED_BRIGHTNESS |
|
|
FEATURE_RESET_FILTER |
|
|
FEATURE_SET_EXTRA_FEATURES)
|
|
|
|
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_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,
|
|
})
|
|
|
|
SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
|
vol.Required(ATTR_BRIGHTNESS):
|
|
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=2))
|
|
})
|
|
|
|
SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
|
vol.Required(ATTR_LEVEL):
|
|
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'},
|
|
SERVICE_SET_LED_ON: {'method': 'async_set_led_on'},
|
|
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_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'},
|
|
}
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities,
|
|
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
|
|
|
|
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)
|
|
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
|
|
elif model.startswith('zhimi.humidifier.'):
|
|
from miio import AirHumidifier
|
|
air_humidifier = AirHumidifier(host, token, model=model)
|
|
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
|
|
elif model.startswith('zhimi.airfresh.'):
|
|
from miio import AirFresh
|
|
air_fresh = AirFresh(host, token)
|
|
device = XiaomiAirFresh(name, air_fresh, 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_entities([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[DATA_KEY].values() if
|
|
device.entity_id in entity_ids]
|
|
else:
|
|
devices = hass.data[DATA_KEY].values()
|
|
|
|
update_tasks = []
|
|
for device in devices:
|
|
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:
|
|
await asyncio.wait(update_tasks, loop=hass.loop)
|
|
|
|
for air_purifier_service in SERVICE_TO_METHOD:
|
|
schema = SERVICE_TO_METHOD[air_purifier_service].get(
|
|
'schema', AIRPURIFIER_SERVICE_SCHEMA)
|
|
hass.services.async_register(
|
|
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
|
|
|
|
|
|
class XiaomiGenericDevice(FanEntity):
|
|
"""Representation of a generic Xiaomi device."""
|
|
|
|
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._available = False
|
|
self._state = None
|
|
self._state_attrs = {
|
|
ATTR_MODEL: self._model,
|
|
}
|
|
self._device_features = FEATURE_SET_CHILD_LOCK
|
|
self._skip_update = False
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
return SUPPORT_SET_SPEED
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""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."""
|
|
return self._name
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true when state is known."""
|
|
return self._available
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes of the device."""
|
|
return self._state_attrs
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if device is on."""
|
|
return self._state
|
|
|
|
@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 = await self.hass.async_add_executor_job(
|
|
partial(func, *args, **kwargs))
|
|
|
|
_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
|
|
|
|
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 = await self.async_set_speed(speed)
|
|
else:
|
|
result = await self._try_command(
|
|
"Turning the miio device on failed.", self._device.on)
|
|
|
|
if result:
|
|
self._state = True
|
|
self._skip_update = True
|
|
|
|
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
|
|
|
|
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_PRO_V7:
|
|
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7
|
|
self._available_attributes = \
|
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
|
|
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7
|
|
elif self._model == MODEL_AIRPURIFIER_2S:
|
|
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
|
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S
|
|
self._speed_list = OPERATION_MODES_AIRPURIFIER_2S
|
|
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
|
|
|
|
# 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()})
|
|
|
|
except DeviceException as ex:
|
|
self._available = False
|
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
|
|
|
@property
|
|
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.airpurifier 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.airpurifier 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_on(self):
|
|
"""Turn the led on."""
|
|
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)
|
|
|
|
async def async_set_led_off(self):
|
|
"""Turn the led off."""
|
|
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, False)
|
|
|
|
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
|
|
|
|
await self._try_command(
|
|
"Setting the led brightness of the miio device failed.",
|
|
self._device.set_led_brightness, LedBrightness(brightness))
|
|
|
|
async def async_set_favorite_level(self, level: int = 1):
|
|
"""Set the favorite 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.airhumidifier 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 if
|
|
mode is not OperationMode.Strong]
|
|
else:
|
|
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
|
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
|
|
self._speed_list = [mode.name for mode in OperationMode if
|
|
mode is not OperationMode.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_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()})
|
|
|
|
except DeviceException as ex:
|
|
self._available = False
|
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
|
|
|
@property
|
|
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)
|
|
|
|
|
|
class XiaomiAirFresh(XiaomiGenericDevice):
|
|
"""Representation of a Xiaomi Air Fresh."""
|
|
|
|
def __init__(self, name, device, model, unique_id):
|
|
"""Initialize the miio device."""
|
|
super().__init__(name, device, model, unique_id)
|
|
|
|
self._device_features = FEATURE_FLAGS_AIRFRESH
|
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH
|
|
self._speed_list = OPERATION_MODES_AIRFRESH
|
|
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_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()})
|
|
|
|
except DeviceException as ex:
|
|
self._available = False
|
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
|
|
|
@property
|
|
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.airfresh 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.airfresh 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_on(self):
|
|
"""Turn the led on."""
|
|
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)
|
|
|
|
async def async_set_led_off(self):
|
|
"""Turn the led off."""
|
|
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, False)
|
|
|
|
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.airfresh 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_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)
|