Add Xiaomi Miio fan config flow (#46866)

* Miio fan config flow

* fix styling and imports

* Update homeassistant/components/xiaomi_miio/fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* rename device -> entity

* fix indent

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
starkillerOG 2021-02-25 04:25:06 +01:00 committed by GitHub
parent 7e35af5d4e
commit 7dc9071776
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 153 deletions

View file

@ -16,6 +16,7 @@ from .const import (
CONF_MODEL, CONF_MODEL,
DOMAIN, DOMAIN,
KEY_COORDINATOR, KEY_COORDINATOR,
MODELS_FAN,
MODELS_SWITCH, MODELS_SWITCH,
MODELS_VACUUM, MODELS_VACUUM,
) )
@ -25,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
SWITCH_PLATFORMS = ["switch"] SWITCH_PLATFORMS = ["switch"]
FAN_PLATFORMS = ["fan"]
VACUUM_PLATFORMS = ["vacuum"] VACUUM_PLATFORMS = ["vacuum"]
@ -122,6 +124,8 @@ async def async_setup_device_entry(
platforms = [] platforms = []
if model in MODELS_SWITCH: if model in MODELS_SWITCH:
platforms = SWITCH_PLATFORMS platforms = SWITCH_PLATFORMS
elif model in MODELS_FAN:
platforms = FAN_PLATFORMS
for vacuum_model in MODELS_VACUUM: for vacuum_model in MODELS_VACUUM:
if model.startswith(vacuum_model): if model.startswith(vacuum_model):
platforms = VACUUM_PLATFORMS platforms = VACUUM_PLATFORMS

View file

@ -73,6 +73,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
return await self.async_step_device() return await self.async_step_device()
for device_model in MODELS_ALL_DEVICES: for device_model in MODELS_ALL_DEVICES:
if name.startswith(device_model.replace(".", "-")): if name.startswith(device_model.replace(".", "-")):
unique_id = format_mac(self.mac) unique_id = format_mac(self.mac)

View file

@ -9,6 +9,53 @@ CONF_MAC = "mac"
KEY_COORDINATOR = "coordinator" KEY_COORDINATOR = "coordinator"
# Fam Models
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_AIRPURIFIER_3 = "zhimi.airpurifier.ma4"
MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4"
MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
MODELS_PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H]
MODELS_HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]
MODELS_FAN_MIIO = [
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_CA1,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRFRESH_VA2,
]
# Model lists
MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"] MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"]
MODELS_SWITCH = [ MODELS_SWITCH = [
"chuangmi.plug.v1", "chuangmi.plug.v1",
@ -23,9 +70,10 @@ MODELS_SWITCH = [
"chuangmi.plug.hmi206", "chuangmi.plug.hmi206",
"lumi.acpartner.v3", "lumi.acpartner.v3",
] ]
MODELS_FAN = MODELS_FAN_MIIO + MODELS_HUMIDIFIER_MIOT + MODELS_PURIFIER_MIOT
MODELS_VACUUM = ["roborock.vacuum"] MODELS_VACUUM = ["roborock.vacuum"]
MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_FAN + MODELS_VACUUM
MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY
# Fan Services # Fan Services

View file

@ -10,7 +10,6 @@ from miio import ( # pylint: disable=import-error
AirHumidifierMiot, AirHumidifierMiot,
AirPurifier, AirPurifier,
AirPurifierMiot, AirPurifierMiot,
Device,
DeviceException, DeviceException,
) )
from miio.airfresh import ( # pylint: disable=import-error, import-error from miio.airfresh import ( # pylint: disable=import-error, import-error
@ -44,6 +43,7 @@ from homeassistant.components.fan import (
SUPPORT_SET_SPEED, SUPPORT_SET_SPEED,
FanEntity, FanEntity,
) )
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_MODE, ATTR_MODE,
@ -51,11 +51,24 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_TOKEN, CONF_TOKEN,
) )
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import ( from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
DOMAIN, DOMAIN,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRPURIFIER_2S,
MODEL_AIRPURIFIER_3,
MODEL_AIRPURIFIER_3H,
MODEL_AIRPURIFIER_PRO,
MODEL_AIRPURIFIER_PRO_V7,
MODEL_AIRPURIFIER_V3,
MODELS_FAN,
MODELS_HUMIDIFIER_MIOT,
MODELS_PURIFIER_MIOT,
SERVICE_RESET_FILTER, SERVICE_RESET_FILTER,
SERVICE_SET_AUTO_DETECT_OFF, SERVICE_SET_AUTO_DETECT_OFF,
SERVICE_SET_AUTO_DETECT_ON, SERVICE_SET_AUTO_DETECT_ON,
@ -77,6 +90,7 @@ from .const import (
SERVICE_SET_TARGET_HUMIDITY, SERVICE_SET_TARGET_HUMIDITY,
SERVICE_SET_VOLUME, SERVICE_SET_VOLUME,
) )
from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -84,58 +98,14 @@ DEFAULT_NAME = "Xiaomi Miio Device"
DATA_KEY = "fan.xiaomi_miio" DATA_KEY = "fan.xiaomi_miio"
CONF_MODEL = "model" 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_AIRPURIFIER_3 = "zhimi.airpurifier.ma4"
MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4"
MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
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( vol.Optional(CONF_MODEL): vol.In(MODELS_FAN),
[
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_AIRPURIFIER_3,
MODEL_AIRPURIFIER_3H,
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRFRESH_VA2,
]
),
} }
) )
@ -193,9 +163,6 @@ ATTR_FAULT = "fault"
# Air Fresh # Air Fresh
ATTR_CO2 = "co2" ATTR_CO2 = "co2"
PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H]
HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]
# Map attributes to properties of the state object # Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_TEMPERATURE: "temperature", ATTR_TEMPERATURE: "temperature",
@ -553,104 +520,114 @@ SERVICE_TO_METHOD = {
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the miio fan device from config.""" """Import Miio configuration from YAML."""
if DATA_KEY not in hass.data: _LOGGER.warning(
hass.data[DATA_KEY] = {} "Loading Xiaomi Miio Fan via platform setup is deprecated. "
"Please remove it from your configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
host = config[CONF_HOST]
token = config[CONF_TOKEN]
name = config[CONF_NAME]
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) async def async_setup_entry(hass, config_entry, async_add_entities):
unique_id = None """Set up the Fan from a config entry."""
entities = []
if model is None: if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
try: if DATA_KEY not in hass.data:
miio_device = Device(host, token) hass.data[DATA_KEY] = {}
device_info = await hass.async_add_executor_job(miio_device.info)
model = device_info.model host = config_entry.data[CONF_HOST]
unique_id = f"{model}-{device_info.mac_address}" token = config_entry.data[CONF_TOKEN]
_LOGGER.info( name = config_entry.title
"%s %s %s detected", model = config_entry.data[CONF_MODEL]
model, unique_id = config_entry.unique_id
device_info.firmware_version,
device_info.hardware_version, _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in MODELS_PURIFIER_MIOT:
air_purifier = AirPurifierMiot(host, token)
entity = XiaomiAirPurifierMiot(name, air_purifier, config_entry, unique_id)
elif model.startswith("zhimi.airpurifier."):
air_purifier = AirPurifier(host, token)
entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id)
elif model in MODELS_HUMIDIFIER_MIOT:
air_humidifier = AirHumidifierMiot(host, token)
entity = XiaomiAirHumidifierMiot(
name, air_humidifier, config_entry, unique_id
) )
except DeviceException as ex: elif model.startswith("zhimi.humidifier."):
raise PlatformNotReady from ex air_humidifier = AirHumidifier(host, token, model=model)
entity = XiaomiAirHumidifier(name, air_humidifier, config_entry, unique_id)
if model in PURIFIER_MIOT: elif model.startswith("zhimi.airfresh."):
air_purifier = AirPurifierMiot(host, token) air_fresh = AirFresh(host, token)
device = XiaomiAirPurifierMiot(name, air_purifier, model, unique_id) entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id)
elif model.startswith("zhimi.airpurifier."):
air_purifier = AirPurifier(host, token)
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
elif model in HUMIDIFIER_MIOT:
air_humidifier = AirHumidifierMiot(host, token)
device = XiaomiAirHumidifierMiot(name, air_humidifier, model, unique_id)
elif model.startswith("zhimi.humidifier."):
air_humidifier = AirHumidifier(host, token, model=model)
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
elif model.startswith("zhimi.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: else:
devices = hass.data[DATA_KEY].values() _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
update_tasks = [] hass.data[DATA_KEY][host] = entity
for device in devices: entities.append(entity)
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: async def async_service_handler(service):
await asyncio.wait(update_tasks) """Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD[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:
entities = [
entity
for entity in hass.data[DATA_KEY].values()
if entity.entity_id in entity_ids
]
else:
entities = hass.data[DATA_KEY].values()
for air_purifier_service in SERVICE_TO_METHOD: update_tasks = []
schema = SERVICE_TO_METHOD[air_purifier_service].get(
"schema", AIRPURIFIER_SERVICE_SCHEMA for entity in entities:
) entity_method = getattr(entity, method["method"], None)
hass.services.async_register( if not entity_method:
DOMAIN, air_purifier_service, async_service_handler, schema=schema continue
) await entity_method(**params)
update_tasks.append(
hass.async_create_task(entity.async_update_ha_state(True))
)
if update_tasks:
await asyncio.wait(update_tasks)
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
)
async_add_entities(entities, update_before_add=True)
class XiaomiGenericDevice(FanEntity): class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
"""Representation of a generic Xiaomi device.""" """Representation of a generic Xiaomi device."""
def __init__(self, name, device, model, unique_id): def __init__(self, name, device, entry, unique_id):
"""Initialize the generic Xiaomi device.""" """Initialize the generic Xiaomi device."""
self._name = name super().__init__(name, device, entry, unique_id)
self._device = device
self._model = model
self._unique_id = unique_id
self._available = False self._available = False
self._state = None self._state = None
@ -668,16 +645,6 @@ class XiaomiGenericDevice(FanEntity):
"""Poll the device.""" """Poll the device."""
return True 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 @property
def available(self): def available(self):
"""Return true when state is known.""" """Return true when state is known."""
@ -803,9 +770,9 @@ class XiaomiGenericDevice(FanEntity):
class XiaomiAirPurifier(XiaomiGenericDevice): class XiaomiAirPurifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Purifier.""" """Representation of a Xiaomi Air Purifier."""
def __init__(self, name, device, model, unique_id): def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch.""" """Initialize the plug switch."""
super().__init__(name, device, model, unique_id) super().__init__(name, device, entry, unique_id)
if self._model == MODEL_AIRPURIFIER_PRO: if self._model == MODEL_AIRPURIFIER_PRO:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
@ -1056,9 +1023,9 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
class XiaomiAirHumidifier(XiaomiGenericDevice): class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier.""" """Representation of a Xiaomi Air Humidifier."""
def __init__(self, name, device, model, unique_id): def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch.""" """Initialize the plug switch."""
super().__init__(name, device, model, unique_id) super().__init__(name, device, entry, unique_id)
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
@ -1214,7 +1181,6 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
async def async_set_speed(self, speed: str) -> None: async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
await self._try_command( await self._try_command(
"Setting operation mode of the miio device failed.", "Setting operation mode of the miio device failed.",
self._device.set_mode, self._device.set_mode,
@ -1247,9 +1213,9 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
class XiaomiAirFresh(XiaomiGenericDevice): class XiaomiAirFresh(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Fresh.""" """Representation of a Xiaomi Air Fresh."""
def __init__(self, name, device, model, unique_id): def __init__(self, name, device, entry, unique_id):
"""Initialize the miio device.""" """Initialize the miio device."""
super().__init__(name, device, model, unique_id) super().__init__(name, device, entry, unique_id)
self._device_features = FEATURE_FLAGS_AIRFRESH self._device_features = FEATURE_FLAGS_AIRFRESH
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH