Xiaomi_Miio Humidifier rework (#52366)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Teemu R. <tpr@iki.fi>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Jan Bouwhuis 2021-07-28 10:52:43 +02:00 committed by GitHub
parent f3e7fb5798
commit 781015fb19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1379 additions and 527 deletions

View file

@ -1205,8 +1205,11 @@ omit =
homeassistant/components/xiaomi_miio/device_tracker.py
homeassistant/components/xiaomi_miio/fan.py
homeassistant/components/xiaomi_miio/gateway.py
homeassistant/components/xiaomi_miio/humidifier.py
homeassistant/components/xiaomi_miio/light.py
homeassistant/components/xiaomi_miio/number.py
homeassistant/components/xiaomi_miio/remote.py
homeassistant/components/xiaomi_miio/select.py
homeassistant/components/xiaomi_miio/sensor.py
homeassistant/components/xiaomi_miio/switch.py
homeassistant/components/xiaomi_miio/vacuum.py

View file

@ -2,12 +2,15 @@
from datetime import timedelta
import logging
import async_timeout
from miio import AirHumidifier, AirHumidifierMiot, DeviceException
from miio.gateway.gateway import GatewayException
from homeassistant import config_entries, core
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_AVAILABLE,
@ -17,8 +20,12 @@ from .const import (
CONF_MODEL,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODELS_AIR_MONITOR,
MODELS_FAN,
MODELS_HUMIDIFIER,
MODELS_HUMIDIFIER_MIOT,
MODELS_LIGHT,
MODELS_SWITCH,
MODELS_VACUUM,
@ -30,6 +37,7 @@ _LOGGER = logging.getLogger(__name__)
GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"]
SWITCH_PLATFORMS = ["switch"]
FAN_PLATFORMS = ["fan"]
HUMIDIFIER_PLATFORMS = ["humidifier", "number", "select", "sensor", "switch"]
LIGHT_PLATFORMS = ["light"]
VACUUM_PLATFORMS = ["vacuum"]
AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"]
@ -51,6 +59,7 @@ async def async_setup_entry(
)
@callback
def get_platforms(config_entry):
"""Return the platforms belonging to a config_entry."""
model = config_entry.data[CONF_MODEL]
@ -61,6 +70,8 @@ def get_platforms(config_entry):
if flow_type == CONF_DEVICE:
if model in MODELS_SWITCH:
return SWITCH_PLATFORMS
if model in MODELS_HUMIDIFIER:
return HUMIDIFIER_PLATFORMS
if model in MODELS_FAN:
return FAN_PLATFORMS
if model in MODELS_LIGHT:
@ -71,10 +82,70 @@ def get_platforms(config_entry):
for air_monitor_model in MODELS_AIR_MONITOR:
if model.startswith(air_monitor_model):
return AIR_MONITOR_PLATFORMS
_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 []
async def async_create_miio_device_and_coordinator(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up a data coordinator and one miio device to service multiple entities."""
model = entry.data[CONF_MODEL]
host = entry.data[CONF_HOST]
token = entry.data[CONF_TOKEN]
name = entry.title
device = None
migrate_entity_name = None
if model not in MODELS_HUMIDIFIER:
return
if model in MODELS_HUMIDIFIER_MIOT:
device = AirHumidifierMiot(host, token)
else:
device = AirHumidifier(host, token, model=model)
# Removing fan platform entity for humidifiers and cache the name and entity name for migration
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id)
if entity_id:
# This check is entities that have a platform migration only and should be removed in the future
migrate_entity_name = entity_registry.async_get(entity_id).name
entity_registry.async_remove(entity_id)
async def async_update_data():
"""Fetch data from the device using async_add_executor_job."""
try:
async with async_timeout.timeout(10):
return await hass.async_add_executor_job(device.status)
except DeviceException as ex:
raise UpdateFailed(ex) from ex
# Create update miio device and coordinator
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=name,
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=60),
)
hass.data[DOMAIN][entry.entry_id] = {
KEY_DEVICE: device,
KEY_COORDINATOR: coordinator,
}
if migrate_entity_name:
hass.data[DOMAIN][entry.entry_id][KEY_MIGRATE_ENTITY_NAME] = migrate_entity_name
# Trigger first data fetch
await coordinator.async_config_entry_first_refresh()
async def async_setup_gateway_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
@ -130,7 +201,6 @@ async def async_setup_gateway_entry(
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name=name,
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
@ -155,6 +225,7 @@ async def async_setup_device_entry(
):
"""Set up the Xiaomi Miio device component from a config entry."""
platforms = get_platforms(entry)
await async_create_miio_device_and_coordinator(hass, entry)
if not platforms:
return False

View file

@ -15,10 +15,17 @@ CONF_MANUAL = "manual"
# Options flow
CONF_CLOUD_SUBDEVICES = "cloud_subdevices"
# Keys
KEY_COORDINATOR = "coordinator"
KEY_DEVICE = "device"
KEY_MIGRATE_ENTITY_NAME = "migrate_entity_name"
# Attributes
ATTR_AVAILABLE = "available"
# Status
SUCCESS = ["ok"]
# Cloud
SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"]
DEFAULT_CLOUD_COUNTRY = "cn"
@ -70,10 +77,12 @@ MODELS_FAN_MIIO = [
MODEL_AIRPURIFIER_SA2,
MODEL_AIRPURIFIER_2S,
MODEL_AIRPURIFIER_2H,
MODEL_AIRFRESH_VA2,
]
MODELS_HUMIDIFIER_MIIO = [
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRFRESH_VA2,
]
# AirQuality Models
@ -108,7 +117,8 @@ MODELS_SWITCH = [
"chuangmi.plug.hmi205",
"chuangmi.plug.hmi206",
]
MODELS_FAN = MODELS_FAN_MIIO + MODELS_HUMIDIFIER_MIOT + MODELS_PURIFIER_MIOT
MODELS_FAN = MODELS_FAN_MIIO + MODELS_PURIFIER_MIOT
MODELS_HUMIDIFIER = MODELS_HUMIDIFIER_MIOT + MODELS_HUMIDIFIER_MIIO
MODELS_LIGHT = (
MODELS_LIGHT_EYECARE
+ MODELS_LIGHT_CEILING
@ -125,17 +135,29 @@ MODELS_AIR_MONITOR = [
]
MODELS_ALL_DEVICES = (
MODELS_SWITCH + MODELS_VACUUM + MODELS_AIR_MONITOR + MODELS_FAN + MODELS_LIGHT
MODELS_SWITCH
+ MODELS_VACUUM
+ MODELS_AIR_MONITOR
+ MODELS_FAN
+ MODELS_HUMIDIFIER
+ MODELS_LIGHT
)
MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY
# Fan Services
# Fan/Humidifier Services
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"
SERVICE_SET_BUZZER = "set_buzzer"
SERVICE_SET_CLEAN_ON = "set_clean_on"
SERVICE_SET_CLEAN_OFF = "set_clean_off"
SERVICE_SET_CLEAN = "set_clean"
SERVICE_SET_FAN_LED_ON = "fan_set_led_on"
SERVICE_SET_FAN_LED_OFF = "fan_set_led_off"
SERVICE_SET_FAN_LED = "fan_set_led"
SERVICE_SET_LED_BRIGHTNESS = "set_led_brightness"
SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on"
SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off"
SERVICE_SET_CHILD_LOCK = "set_child_lock"
SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness"
SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level"
SERVICE_SET_FAN_LEVEL = "fan_set_fan_level"
@ -149,6 +171,7 @@ SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features"
SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity"
SERVICE_SET_DRY_ON = "fan_set_dry_on"
SERVICE_SET_DRY_OFF = "fan_set_dry_off"
SERVICE_SET_DRY = "set_dry"
SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed"
# Light Services
@ -180,3 +203,95 @@ SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop"
SERVICE_CLEAN_SEGMENT = "vacuum_clean_segment"
SERVICE_CLEAN_ZONE = "vacuum_clean_zone"
SERVICE_GOTO = "vacuum_goto"
# Features
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_SET_FAN_LEVEL = 4096
FEATURE_SET_MOTOR_SPEED = 8192
FEATURE_SET_CLEAN = 16384
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_3 = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
| FEATURE_SET_LED
| FEATURE_SET_FAVORITE_LEVEL
| FEATURE_SET_FAN_LEVEL
| FEATURE_SET_LED_BRIGHTNESS
)
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
| FEATURE_SET_LED_BRIGHTNESS
| FEATURE_SET_TARGET_HUMIDITY
)
FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY
FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
| FEATURE_SET_LED_BRIGHTNESS
| FEATURE_SET_TARGET_HUMIDITY
| FEATURE_SET_DRY
| FEATURE_SET_MOTOR_SPEED
| FEATURE_SET_CLEAN
)
FEATURE_FLAGS_AIRFRESH = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
| FEATURE_SET_LED
| FEATURE_SET_LED_BRIGHTNESS
| FEATURE_RESET_FILTER
| FEATURE_SET_EXTRA_FEATURES
)

View file

@ -1,4 +1,5 @@
"""Code to handle a Xiaomi Device."""
from functools import partial
import logging
from construct.core import ChecksumError
@ -7,6 +8,7 @@ from miio import Device, DeviceException
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_MAC, CONF_MODEL, DOMAIN
@ -73,6 +75,7 @@ class XiaomiMiioEntity(Entity):
self._device_id = entry.unique_id
self._unique_id = unique_id
self._name = name
self._available = None
@property
def unique_id(self):
@ -98,3 +101,59 @@ class XiaomiMiioEntity(Entity):
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)}
return device_info
class XiaomiCoordinatedMiioEntity(CoordinatorEntity):
"""Representation of a base a coordinated Xiaomi Miio Entity."""
def __init__(self, name, device, entry, unique_id, coordinator):
"""Initialize the coordinated Xiaomi Miio Device."""
super().__init__(coordinator)
self._device = device
self._model = entry.data[CONF_MODEL]
self._mac = entry.data[CONF_MAC]
self._device_id = entry.unique_id
self._device_name = entry.title
self._unique_id = unique_id
self._name = name
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of this entity, if any."""
return self._name
@property
def device_info(self):
"""Return the device info."""
device_info = {
"identifiers": {(DOMAIN, self._device_id)},
"manufacturer": "Xiaomi",
"name": self._device_name,
"model": self._model,
}
if self._mac is not None:
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)}
return device_info
async def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a miio device command handling error messages."""
try:
result = await self.hass.async_add_executor_job(
partial(func, *args, **kwargs)
)
_LOGGER.debug("Response received from miio device: %s", result)
return True
except DeviceException as exc:
if self.available:
_LOGGER.error(mask_error, exc)
return False

View file

@ -5,27 +5,11 @@ from functools import partial
import logging
import math
from miio import (
AirFresh,
AirHumidifier,
AirHumidifierMiot,
AirPurifier,
AirPurifierMiot,
DeviceException,
)
from miio import AirFresh, AirPurifier, AirPurifierMiot, DeviceException
from miio.airfresh import (
LedBrightness as AirfreshLedBrightness,
OperationMode as AirfreshOperationMode,
)
from miio.airhumidifier import (
LedBrightness as AirhumidifierLedBrightness,
OperationMode as AirhumidifierOperationMode,
)
from miio.airhumidifier_miot import (
LedBrightness as AirhumidifierMiotLedBrightness,
OperationMode as AirhumidifierMiotOperationMode,
PressedButton as AirhumidifierPressedButton,
)
from miio.airpurifier import (
LedBrightness as AirpurifierLedBrightness,
OperationMode as AirpurifierOperationMode,
@ -38,9 +22,6 @@ import voluptuous as vol
from homeassistant.components.fan import (
PLATFORM_SCHEMA,
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED,
FanEntity,
@ -64,16 +45,23 @@ from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
DOMAIN,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
FEATURE_RESET_FILTER,
FEATURE_SET_AUTO_DETECT,
FEATURE_SET_BUZZER,
FEATURE_SET_CHILD_LOCK,
FEATURE_SET_EXTRA_FEATURES,
FEATURE_SET_FAN_LEVEL,
FEATURE_SET_FAVORITE_LEVEL,
FEATURE_SET_LEARN_MODE,
FEATURE_SET_LED,
FEATURE_SET_LED_BRIGHTNESS,
FEATURE_SET_VOLUME,
MODEL_AIRPURIFIER_2H,
MODEL_AIRPURIFIER_2S,
MODEL_AIRPURIFIER_PRO,
MODEL_AIRPURIFIER_PRO_V7,
MODEL_AIRPURIFIER_V3,
MODELS_FAN,
MODELS_HUMIDIFIER_MIOT,
MODELS_PURIFIER_MIOT,
SERVICE_RESET_FILTER,
SERVICE_SET_AUTO_DETECT_OFF,
@ -82,8 +70,6 @@ from .const import (
SERVICE_SET_BUZZER_ON,
SERVICE_SET_CHILD_LOCK_OFF,
SERVICE_SET_CHILD_LOCK_ON,
SERVICE_SET_DRY_OFF,
SERVICE_SET_DRY_ON,
SERVICE_SET_EXTRA_FEATURES,
SERVICE_SET_FAN_LED_OFF,
SERVICE_SET_FAN_LED_ON,
@ -92,9 +78,8 @@ from .const import (
SERVICE_SET_LEARN_MODE_OFF,
SERVICE_SET_LEARN_MODE_ON,
SERVICE_SET_LED_BRIGHTNESS,
SERVICE_SET_MOTOR_SPEED,
SERVICE_SET_TARGET_HUMIDITY,
SERVICE_SET_VOLUME,
SUCCESS,
)
from .device import XiaomiMiioEntity
@ -150,20 +135,6 @@ 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 Humidifier CA4
ATTR_ACTUAL_MOTOR_SPEED = "actual_speed"
ATTR_FAHRENHEIT = "fahrenheit"
# Air Fresh
ATTR_CO2 = "co2"
@ -283,41 +254,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
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",
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
ATTR_TRANS_LEVEL: "trans_level",
ATTR_BUTTON_PRESSED: "button_pressed",
ATTR_HARDWARE_VERSION: "hardware_version",
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
ATTR_MOTOR_SPEED: "motor_speed",
ATTR_DEPTH: "depth",
ATTR_DRY: "dry",
ATTR_HARDWARE_VERSION: "hardware_version",
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
ATTR_ACTUAL_MOTOR_SPEED: "actual_speed",
ATTR_BUTTON_PRESSED: "button_pressed",
ATTR_DRY: "dry",
ATTR_FAHRENHEIT: "fahrenheit",
ATTR_MOTOR_SPEED: "motor_speed",
}
AVAILABLE_ATTRIBUTES_AIRFRESH = {
ATTR_TEMPERATURE: "temperature",
ATTR_AIR_QUALITY_INDEX: "aqi",
@ -365,25 +301,6 @@ PRESET_MODES_AIRPURIFIER_V3 = [
]
OPERATION_MODES_AIRFRESH = ["Auto", "Silent", "Interval", "Low", "Middle", "Strong"]
PRESET_MODES_AIRFRESH = ["Auto", "Interval"]
PRESET_MODES_AIRHUMIDIFIER = ["Auto"]
PRESET_MODES_AIRHUMIDIFIER_CA4 = ["Auto"]
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_SET_FAN_LEVEL = 4096
FEATURE_SET_MOTOR_SPEED = 8192
FEATURE_FLAGS_AIRPURIFIER = (
FEATURE_SET_BUZZER
@ -431,25 +348,6 @@ 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
| FEATURE_SET_LED_BRIGHTNESS
| FEATURE_SET_TARGET_HUMIDITY
)
FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY
FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
| FEATURE_SET_LED_BRIGHTNESS
| FEATURE_SET_TARGET_HUMIDITY
| FEATURE_SET_DRY
| FEATURE_SET_MOTOR_SPEED
)
FEATURE_FLAGS_AIRFRESH = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
@ -481,22 +379,6 @@ SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_FEATURES): cv.positive_int}
)
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_SCHEMA_MOTOR_SPEED = AIRPURIFIER_SERVICE_SCHEMA.extend(
{
vol.Required(ATTR_MOTOR_SPEED): vol.All(
vol.Coerce(int), vol.Clamp(min=200, max=2000)
)
}
)
SERVICE_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {"method": "async_set_buzzer_on"},
SERVICE_SET_BUZZER_OFF: {"method": "async_set_buzzer_off"},
@ -526,16 +408,6 @@ SERVICE_TO_METHOD = {
"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"},
SERVICE_SET_MOTOR_SPEED: {
"method": "async_set_motor_speed",
"schema": SERVICE_SCHEMA_MOTOR_SPEED,
},
}
@ -578,14 +450,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
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
)
elif model.startswith("zhimi.humidifier."):
air_humidifier = AirHumidifier(host, token, model=model)
entity = XiaomiAirHumidifier(name, air_humidifier, config_entry, unique_id)
elif model.startswith("zhimi.airfresh."):
air_fresh = AirFresh(host, token)
entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id)
@ -1247,345 +1111,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
)
class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier."""
SPEED_MODE_MAPPING = {
1: AirhumidifierOperationMode.Silent,
2: AirhumidifierOperationMode.Medium,
3: AirhumidifierOperationMode.High,
4: AirhumidifierOperationMode.Strong,
}
REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()}
PRESET_MODE_MAPPING = {
"Auto": AirhumidifierOperationMode.Auto,
}
def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id)
self._percentage = None
self._preset_mode = None
self._supported_features = SUPPORT_SET_SPEED
self._preset_modes = []
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB
# the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Strong
]
self._supported_features |= SUPPORT_PRESET_MODE
self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
self._speed_count = 3
elif self._model in [MODEL_AIRHUMIDIFIER_CA4]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4
# the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
self._supported_features |= SUPPORT_PRESET_MODE
self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
self._speed_count = 3
else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
# the speed_list attribute is deprecated, support will end with release 2021.7
self._speed_list = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Auto
]
self._supported_features |= SUPPORT_PRESET_MODE
self._preset_modes = PRESET_MODES_AIRHUMIDIFIER
self._speed_count = 4
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}
)
async def async_update(self):
"""Fetch state from the device."""
# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return
try:
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._state_attrs.update(
{
key: self._extract_value_from_attribute(state, value)
for key, value in self._available_attributes.items()
}
)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def preset_mode(self):
"""Get the active preset mode."""
if self._state:
preset_mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name
return preset_mode if preset_mode in self._preset_modes else None
return None
@property
def percentage(self):
"""Return the current percentage based speed."""
if self._state:
mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE])
if mode in self.REVERSE_SPEED_MODE_MAPPING:
return ranged_value_to_percentage(
(1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode]
)
return None
# the speed attribute is deprecated, support will end with release 2021.7
@property
def speed(self):
"""Return the current speed."""
if self._state:
return AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name
return None
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan.
This method is a coroutine.
"""
speed_mode = math.ceil(
percentage_to_ranged_value((1, self._speed_count), percentage)
)
if speed_mode:
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
AirhumidifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]),
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan.
This method is a coroutine.
"""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
self.PRESET_MODE_MAPPING[preset_mode],
)
# the async_set_speed function is deprecated, support will end with release 2021.7
# it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
return
_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,
AirhumidifierOperationMode[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
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierLedBrightness(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 XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
"""Representation of a Xiaomi Air Humidifier (MiOT protocol)."""
PRESET_MODE_MAPPING = {
AirhumidifierMiotOperationMode.Auto: "Auto",
}
REVERSE_PRESET_MODE_MAPPING = {v: k for k, v in PRESET_MODE_MAPPING.items()}
SPEED_MAPPING = {
AirhumidifierMiotOperationMode.Low: SPEED_LOW,
AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM,
AirhumidifierMiotOperationMode.High: SPEED_HIGH,
}
REVERSE_SPEED_MAPPING = {v: k for k, v in SPEED_MAPPING.items()}
SPEEDS = [
AirhumidifierMiotOperationMode.Low,
AirhumidifierMiotOperationMode.Mid,
AirhumidifierMiotOperationMode.High,
]
# the speed attribute is deprecated, support will end with release 2021.7
# it is added here for compatibility
@property
def speed(self):
"""Return current legacy speed."""
if (
self.state
and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
in self.SPEED_MAPPING
):
return self.SPEED_MAPPING[
AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
]
return None
@property
def percentage(self):
"""Return the current percentage based speed."""
if (
self.state
and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
in self.SPEEDS
):
return ranged_value_to_percentage(
(1, self.speed_count), self._state_attrs[ATTR_MODE]
)
return None
@property
def preset_mode(self):
"""Return the current preset_mode."""
if self._state:
mode = self.PRESET_MODE_MAPPING.get(
AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
)
if mode in self._preset_modes:
return mode
return None
@property
def button_pressed(self):
"""Return the last button pressed."""
if self._state:
return AirhumidifierPressedButton(
self._state_attrs[ATTR_BUTTON_PRESSED]
).name
return None
# the async_set_speed function is deprecated, support will end with release 2021.7
# it is added here only for compatibility with legacy speeds
async def async_set_speed(self, speed: str) -> None:
"""Override for set async_set_speed of the super() class."""
if speed and speed in self.REVERSE_SPEED_MAPPING:
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
self.REVERSE_SPEED_MAPPING[speed],
)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan.
This method is a coroutine.
"""
mode = math.ceil(percentage_to_ranged_value((1, 3), percentage))
if mode:
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
AirhumidifierMiotOperationMode(mode),
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan.
This method is a coroutine.
"""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
self.REVERSE_PRESET_MODE_MAPPING[preset_mode],
)
async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierMiotLedBrightness(brightness),
)
async def async_set_motor_speed(self, motor_speed: int = 400):
"""Set the target motor speed."""
if self._device_features & FEATURE_SET_MOTOR_SPEED == 0:
return
await self._try_command(
"Setting the target motor speed of the miio device failed.",
self._device.set_speed,
motor_speed,
)
class XiaomiAirFresh(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Fresh."""

View file

@ -0,0 +1,372 @@
"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity."""
from enum import Enum
import logging
import math
from miio.airhumidifier import OperationMode as AirhumidifierOperationMode
from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode
from homeassistant.components.humidifier import HumidifierEntity
from homeassistant.components.humidifier.const import (
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_HUMIDIFIER,
SUPPORT_MODES,
)
from homeassistant.const import ATTR_MODE, CONF_HOST, CONF_TOKEN
from homeassistant.core import callback
from homeassistant.util.percentage import percentage_to_ranged_value
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_MODEL,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODELS_HUMIDIFIER_MIOT,
)
from .device import XiaomiCoordinatedMiioEntity
_LOGGER = logging.getLogger(__name__)
# Air Humidifier
ATTR_TARGET_HUMIDITY = "target_humidity"
AVAILABLE_ATTRIBUTES = {
ATTR_MODE: "mode",
ATTR_TARGET_HUMIDITY: "target_humidity",
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Humidifier from a config entry."""
if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
return
entities = []
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
else:
name = config_entry.title
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in MODELS_HUMIDIFIER_MIOT:
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiAirHumidifierMiot(
name,
air_humidifier,
config_entry,
unique_id,
coordinator,
)
else:
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiAirHumidifier(
name,
air_humidifier,
config_entry,
unique_id,
coordinator,
)
entities.append(entity)
async_add_entities(entities)
class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity):
"""Representation of a generic Xiaomi humidifier device."""
_attr_device_class = DEVICE_CLASS_HUMIDIFIER
_attr_supported_features = SUPPORT_MODES
def __init__(self, name, device, entry, unique_id, coordinator):
"""Initialize the generic Xiaomi device."""
super().__init__(name, device, entry, unique_id, coordinator=coordinator)
self._state = None
self._attributes = {}
self._available_modes = []
self._mode = None
self._min_humidity = DEFAULT_MIN_HUMIDITY
self._max_humidity = DEFAULT_MAX_HUMIDITY
self._humidity_steps = 100
self._target_humidity = None
@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
@property
def available_modes(self) -> list:
"""Get the list of available modes."""
return self._available_modes
@property
def mode(self):
"""Get the current mode."""
return self._mode
@property
def min_humidity(self):
"""Return the minimum target humidity."""
return self._min_humidity
@property
def max_humidity(self):
"""Return the maximum target humidity."""
return self._max_humidity
async def async_turn_on(
self,
**kwargs,
) -> None:
"""Turn the device on."""
result = await self._try_command(
"Turning the miio device on failed.", self._device.on
)
if result:
self._state = 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
def translate_humidity(self, humidity):
"""Translate the target humidity to the first valid step."""
return (
math.ceil(percentage_to_ranged_value((1, self._humidity_steps), humidity))
* 100
/ self._humidity_steps
if 0 < humidity <= 100
else None
)
class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity):
"""Representation of a Xiaomi Air Humidifier."""
def __init__(self, name, device, entry, unique_id, coordinator):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id, coordinator)
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._available_modes = []
self._available_modes = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Strong
]
self._min_humidity = 30
self._max_humidity = 80
self._humidity_steps = 10
elif self._model in [MODEL_AIRHUMIDIFIER_CA4]:
self._available_modes = [
mode.name for mode in AirhumidifierMiotOperationMode
]
self._min_humidity = 30
self._max_humidity = 80
self._humidity_steps = 100
else:
self._available_modes = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Auto
]
self._min_humidity = 30
self._max_humidity = 80
self._humidity_steps = 10
self._state = self.coordinator.data.is_on
self._attributes.update(
{
key: self._extract_value_from_attribute(self.coordinator.data, value)
for key, value in AVAILABLE_ATTRIBUTES.items()
}
)
self._target_humidity = self._attributes[ATTR_TARGET_HUMIDITY]
self._mode = self._attributes[ATTR_MODE]
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
self._state = self.coordinator.data.is_on
self._attributes.update(
{
key: self._extract_value_from_attribute(self.coordinator.data, value)
for key, value in AVAILABLE_ATTRIBUTES.items()
}
)
self._target_humidity = self._attributes[ATTR_TARGET_HUMIDITY]
self._mode = self._attributes[ATTR_MODE]
self.async_write_ha_state()
@property
def mode(self):
"""Return the current mode."""
return AirhumidifierOperationMode(self._mode).name
@property
def target_humidity(self):
"""Return the target humidity."""
return (
self._target_humidity
if self._mode == AirhumidifierOperationMode.Auto.name
or AirhumidifierOperationMode.Auto.name not in self.available_modes
else None
)
async def async_set_humidity(self, humidity: int) -> None:
"""Set the target humidity of the humidifier and set the mode to auto."""
target_humidity = self.translate_humidity(humidity)
if not target_humidity:
return
_LOGGER.debug("Setting the target humidity to: %s", target_humidity)
if await self._try_command(
"Setting target humidity of the miio device failed.",
self._device.set_target_humidity,
target_humidity,
):
self._target_humidity = target_humidity
if (
self.supported_features & SUPPORT_MODES == 0
or AirhumidifierOperationMode(self._attributes[ATTR_MODE])
== AirhumidifierOperationMode.Auto
or AirhumidifierOperationMode.Auto.name not in self.available_modes
):
self.async_write_ha_state()
return
_LOGGER.debug("Setting the operation mode to: Auto")
if await self._try_command(
"Setting operation mode of the miio device to MODE_AUTO failed.",
self._device.set_mode,
AirhumidifierOperationMode.Auto,
):
self._mode = AirhumidifierOperationMode.Auto.name
self.async_write_ha_state()
async def async_set_mode(self, mode: str) -> None:
"""Set the mode of the humidifier."""
if self.supported_features & SUPPORT_MODES == 0 or not mode:
return
if mode not in self.available_modes:
_LOGGER.warning("Mode %s is not a valid operation mode", mode)
return
_LOGGER.debug("Setting the operation mode to: %s", mode)
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
AirhumidifierOperationMode[mode.title()],
):
self._mode = mode.title()
self.async_write_ha_state()
class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
"""Representation of a Xiaomi Air Humidifier (MiOT protocol)."""
MODE_MAPPING = {
AirhumidifierMiotOperationMode.Auto: "Auto",
AirhumidifierMiotOperationMode.Low: "Low",
AirhumidifierMiotOperationMode.Mid: "Mid",
AirhumidifierMiotOperationMode.High: "High",
}
REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()}
@property
def mode(self):
"""Return the current mode."""
return AirhumidifierMiotOperationMode(self._mode).name
@property
def target_humidity(self):
"""Return the target humidity."""
if self._state:
return (
self._target_humidity
if AirhumidifierMiotOperationMode(self._mode)
== AirhumidifierMiotOperationMode.Auto
else None
)
return None
async def async_set_humidity(self, humidity: int) -> None:
"""Set the target humidity of the humidifier and set the mode to auto."""
target_humidity = self.translate_humidity(humidity)
if not target_humidity:
return
_LOGGER.debug("Setting the humidity to: %s", target_humidity)
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_target_humidity,
target_humidity,
):
self._target_humidity = target_humidity
if (
self.supported_features & SUPPORT_MODES == 0
or AirhumidifierMiotOperationMode(self._attributes[ATTR_MODE])
== AirhumidifierMiotOperationMode.Auto
):
self.async_write_ha_state()
return
_LOGGER.debug("Setting the operation mode to: Auto")
if await self._try_command(
"Setting operation mode of the miio device to MODE_AUTO failed.",
self._device.set_mode,
AirhumidifierMiotOperationMode.Auto,
):
self._mode = 0
self.async_write_ha_state()
async def async_set_mode(self, mode: str) -> None:
"""Set the mode of the fan."""
if self.supported_features & SUPPORT_MODES == 0 or not mode:
return
if mode not in self.REVERSE_MODE_MAPPING:
_LOGGER.warning("Mode %s is not a valid operation mode", mode)
return
_LOGGER.debug("Setting the operation mode to: %s", mode)
if self._state:
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
self.REVERSE_MODE_MAPPING[mode],
):
self._mode = self.REVERSE_MODE_MAPPING[mode].value
self.async_write_ha_state()

View file

@ -0,0 +1,156 @@
"""Motor speed support for Xiaomi Mi Air Humidifier."""
from dataclasses import dataclass
from enum import Enum
import logging
from homeassistant.components.number import NumberEntity
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import callback
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_MODEL,
DOMAIN,
FEATURE_SET_MOTOR_SPEED,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODEL_AIRHUMIDIFIER_CA4,
)
from .device import XiaomiCoordinatedMiioEntity
_LOGGER = logging.getLogger(__name__)
ATTR_MOTOR_SPEED = "motor_speed"
@dataclass
class NumberType:
"""Class that holds device specific info for a xiaomi aqara or humidifier number controller types."""
name: str = None
short_name: str = None
unit_of_measurement: str = None
icon: str = None
device_class: str = None
min: float = None
max: float = None
step: float = None
available_with_device_off: bool = True
NUMBER_TYPES = {
FEATURE_SET_MOTOR_SPEED: NumberType(
name="Motor Speed",
icon="mdi:fast-forward-outline",
short_name=ATTR_MOTOR_SPEED,
unit_of_measurement="rpm",
min=200,
max=2000,
step=10,
available_with_device_off=False,
),
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Selectors from a config entry."""
entities = []
if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
return
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
model = config_entry.data[CONF_MODEL]
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
else:
name = config_entry.title
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model not in [MODEL_AIRHUMIDIFIER_CA4]:
return
for number in NUMBER_TYPES.values():
entities.append(
XiaomiAirHumidifierNumber(
f"{name} {number.name}",
device,
config_entry,
f"{number.short_name}_{config_entry.unique_id}",
number,
coordinator,
)
)
async_add_entities(entities)
class XiaomiAirHumidifierNumber(XiaomiCoordinatedMiioEntity, NumberEntity):
"""Representation of a generic Xiaomi attribute selector."""
def __init__(self, name, device, entry, unique_id, number, coordinator):
"""Initialize the generic Xiaomi attribute selector."""
super().__init__(name, device, entry, unique_id, coordinator)
self._attr_icon = number.icon
self._attr_unit_of_measurement = number.unit_of_measurement
self._attr_min_value = number.min
self._attr_max_value = number.max
self._attr_step = number.step
self._controller = number
self._attr_value = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
@property
def available(self):
"""Return the number controller availability."""
if (
super().available
and not self.coordinator.data.is_on
and not self._controller.available_with_device_off
):
return False
return super().available
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def async_set_value(self, value):
"""Set an option of the miio device."""
if (
self.min_value
and value < self.min_value
or self.max_value
and value > self.max_value
):
raise ValueError(
f"Value {value} not a valid {self.name} within the range {self.min_value} - {self.max_value}"
)
if await self.async_set_motor_speed(value):
self._attr_value = value
self.async_write_ha_state()
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
# On state change the device doesn't provide the new state immediately.
self._attr_value = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
self.async_write_ha_state()
async def async_set_motor_speed(self, motor_speed: int = 400):
"""Set the target motor speed."""
return await self._try_command(
"Setting the target motor speed of the miio device failed.",
self._device.set_speed,
motor_speed,
)

View file

@ -0,0 +1,189 @@
"""Support led_brightness for Mi Air Humidifier."""
from dataclasses import dataclass
from enum import Enum
import logging
from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness
from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness
from homeassistant.components.select import SelectEntity
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import callback
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_MODEL,
DOMAIN,
FEATURE_SET_LED_BRIGHTNESS,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODELS_HUMIDIFIER,
SERVICE_SET_LED_BRIGHTNESS,
)
from .device import XiaomiCoordinatedMiioEntity
_LOGGER = logging.getLogger(__name__)
ATTR_LED_BRIGHTNESS = "led_brightness"
LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2}
LED_BRIGHTNESS_MAP_MIOT = {"Bright": 2, "Dim": 1, "Off": 0}
LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()}
LED_BRIGHTNESS_REVERSE_MAP_MIOT = {
val: key for key, val in LED_BRIGHTNESS_MAP_MIOT.items()
}
@dataclass
class SelectorType:
"""Class that holds device specific info for a xiaomi aqara or humidifier selectors."""
name: str = None
icon: str = None
short_name: str = None
options: list = None
service: str = None
SELECTOR_TYPES = {
FEATURE_SET_LED_BRIGHTNESS: SelectorType(
name="Led brightness",
icon="mdi:brightness-6",
short_name=ATTR_LED_BRIGHTNESS,
options=["Bright", "Dim", "Off"],
service=SERVICE_SET_LED_BRIGHTNESS,
),
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Selectors from a config entry."""
if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
return
entities = []
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
model = config_entry.data[CONF_MODEL]
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
else:
name = config_entry.title
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
entity_class = XiaomiAirHumidifierSelector
elif model in [MODEL_AIRHUMIDIFIER_CA4]:
entity_class = XiaomiAirHumidifierMiotSelector
elif model in MODELS_HUMIDIFIER:
entity_class = XiaomiAirHumidifierSelector
else:
return
for selector in SELECTOR_TYPES.values():
entities.append(
entity_class(
f"{name} {selector.name}",
device,
config_entry,
f"{selector.short_name}_{config_entry.unique_id}",
selector,
coordinator,
)
)
async_add_entities(entities)
class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity):
"""Representation of a generic Xiaomi attribute selector."""
def __init__(self, name, device, entry, unique_id, selector, coordinator):
"""Initialize the generic Xiaomi attribute selector."""
super().__init__(name, device, entry, unique_id, coordinator)
self._attr_icon = selector.icon
self._controller = selector
self._attr_options = self._controller.options
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
class XiaomiAirHumidifierSelector(XiaomiSelector):
"""Representation of a Xiaomi Air Humidifier selector."""
def __init__(self, name, device, entry, unique_id, controller, coordinator):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id, controller, coordinator)
self._current_led_brightness = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
self._current_led_brightness = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
self.async_write_ha_state()
@property
def current_option(self):
"""Return the current option."""
return self.led_brightness
async def async_select_option(self, option: str) -> None:
"""Set an option of the miio device."""
if option not in self.options:
raise ValueError(
f"Selection '{option}' is not a valid {self._controller.name}"
)
await self.async_set_led_brightness(option)
@property
def led_brightness(self):
"""Return the current led brightness."""
return LED_BRIGHTNESS_REVERSE_MAP.get(self._current_led_brightness)
async def async_set_led_brightness(self, brightness: str):
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self.async_write_ha_state()
class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Air Humidifier (MiOT protocol) selector."""
@property
def led_brightness(self):
"""Return the current led brightness."""
return LED_BRIGHTNESS_REVERSE_MAP_MIOT.get(self._current_led_brightness)
async def async_set_led_brightness(self, brightness: str):
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierMiotLedBrightness(LED_BRIGHTNESS_MAP_MIOT[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP_MIOT[brightness]
self.async_write_ha_state()

View file

@ -1,5 +1,6 @@
"""Support for Xiaomi Mi Air Quality Monitor (PM2.5)."""
"""Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier."""
from dataclasses import dataclass
from enum import Enum
import logging
from miio import AirQualityMonitor, DeviceException
@ -20,6 +21,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE,
CONF_HOST,
CONF_NAME,
CONF_TOKEN,
@ -35,8 +37,18 @@ from homeassistant.const import (
)
import homeassistant.helpers.config_validation as cv
from .const import CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR
from .device import XiaomiMiioEntity
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MODEL,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODELS_HUMIDIFIER_MIOT,
)
from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
@ -59,42 +71,69 @@ ATTR_NIGHT_MODE = "night_mode"
ATTR_NIGHT_TIME_BEGIN = "night_time_begin"
ATTR_NIGHT_TIME_END = "night_time_end"
ATTR_SENSOR_STATE = "sensor_state"
SUCCESS = ["ok"]
ATTR_WATER_LEVEL = "water_level"
ATTR_HUMIDITY = "humidity"
ATTR_ACTUAL_MOTOR_SPEED = "actual_speed"
@dataclass
class SensorType:
"""Class that holds device specific info for a xiaomi aqara sensor."""
"""Class that holds device specific info for a xiaomi aqara or humidifier sensor."""
unit: str = None
icon: str = None
device_class: str = None
state_class: str = None
valid_min_value: float = None
valid_max_value: float = None
GATEWAY_SENSOR_TYPES = {
SENSOR_TYPES = {
"temperature": SensorType(
unit=TEMP_CELSIUS,
icon=None,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
"humidity": SensorType(
unit=PERCENTAGE,
icon=None,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
"pressure": SensorType(
unit=PRESSURE_HPA,
icon=None,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
"load_power": SensorType(
unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER
unit=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
),
"water_level": SensorType(
unit=PERCENTAGE,
icon="mdi:water-check",
state_class=STATE_CLASS_MEASUREMENT,
valid_min_value=0.0,
valid_max_value=100.0,
),
"actual_speed": SensorType(
unit="rpm",
icon="mdi:fast-forward",
state_class=STATE_CLASS_MEASUREMENT,
valid_min_value=200.0,
valid_max_value=2000.0,
),
}
HUMIDIFIER_SENSORS = {
ATTR_HUMIDITY: "humidity",
ATTR_TEMPERATURE: "temperature",
}
HUMIDIFIER_SENSORS_MIOT = {
ATTR_HUMIDITY: "humidity",
ATTR_TEMPERATURE: "temperature",
ATTR_WATER_LEVEL: "water_level",
ATTR_ACTUAL_MOTOR_SPEED: "actual_speed",
}
@ -135,7 +174,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sub_devices = gateway.devices
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
for sub_device in sub_devices.values():
sensor_variables = set(sub_device.status) & set(GATEWAY_SENSOR_TYPES)
sensor_variables = set(sub_device.status) & set(SENSOR_TYPES)
if sensor_variables:
entities.extend(
[
@ -145,19 +184,90 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for variable in sensor_variables
]
)
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
unique_id = config_entry.unique_id
model = config_entry.data[CONF_MODEL]
device = None
sensors = []
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
else:
name = config_entry.title
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
if model in MODELS_HUMIDIFIER_MIOT:
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
sensors = HUMIDIFIER_SENSORS_MIOT
elif model.startswith("zhimi.humidifier."):
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
sensors = HUMIDIFIER_SENSORS
else:
unique_id = config_entry.unique_id
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
device = AirQualityMonitor(host, token)
entities.append(XiaomiAirQualityMonitor(name, device, config_entry, unique_id))
device = AirQualityMonitor(host, token)
entities.append(
XiaomiAirQualityMonitor(name, device, config_entry, unique_id)
)
for sensor in sensors:
entities.append(
XiaomiGenericSensor(
f"{name} {sensor.replace('_', ' ').title()}",
device,
config_entry,
f"{sensor}_{config_entry.unique_id}",
sensor,
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR],
)
)
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)
class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity):
"""Representation of a Xiaomi Humidifier sensor."""
def __init__(self, name, device, entry, unique_id, attribute, coordinator):
"""Initialize the entity."""
super().__init__(name, device, entry, unique_id, coordinator)
self._sensor_config = SENSOR_TYPES[attribute]
self._attr_device_class = self._sensor_config.device_class
self._attr_state_class = self._sensor_config.state_class
self._attr_icon = self._sensor_config.icon
self._attr_name = name
self._attr_unique_id = unique_id
self._attr_unit_of_measurement = self._sensor_config.unit
self._device = device
self._entry = entry
self._attribute = attribute
self._state = None
@property
def state(self):
"""Return the state of the device."""
self._state = self._extract_value_from_attribute(
self.coordinator.data, self._attribute
)
if (
self._sensor_config.valid_min_value
and self._state < self._sensor_config.valid_min_value
) or (
self._sensor_config.valid_max_value
and self._state > self._sensor_config.valid_max_value
):
return None
return self._state
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity):
@ -189,7 +299,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity):
@property
def icon(self):
"""Return the icon to use for device if any."""
"""Return the icon to use in the frontend."""
return self._icon
@property
@ -247,22 +357,22 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity):
@property
def icon(self):
"""Return the icon to use in the frontend."""
return GATEWAY_SENSOR_TYPES[self._data_key].icon
return SENSOR_TYPES[self._data_key].icon
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return GATEWAY_SENSOR_TYPES[self._data_key].unit
return SENSOR_TYPES[self._data_key].unit
@property
def device_class(self):
"""Return the device class of this entity."""
return GATEWAY_SENSOR_TYPES[self._data_key].device_class
return SENSOR_TYPES[self._data_key].device_class
@property
def state_class(self):
"""Return the state class of this entity."""
return GATEWAY_SENSOR_TYPES[self._data_key].state_class
return SENSOR_TYPES[self._data_key].state_class
@property
def state(self):

View file

@ -1,5 +1,7 @@
"""Support for Xiaomi Smart WiFi Socket and Smart Power Strip."""
import asyncio
from dataclasses import dataclass
from enum import Enum
from functools import partial
import logging
@ -21,6 +23,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_TOKEN,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import (
@ -29,13 +32,31 @@ from .const import (
CONF_GATEWAY,
CONF_MODEL,
DOMAIN,
FEATURE_FLAGS_AIRHUMIDIFIER,
FEATURE_FLAGS_AIRHUMIDIFIER_CA4,
FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB,
FEATURE_SET_BUZZER,
FEATURE_SET_CHILD_LOCK,
FEATURE_SET_CLEAN,
FEATURE_SET_DRY,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_MIGRATE_ENTITY_NAME,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODELS_HUMIDIFIER,
SERVICE_SET_BUZZER,
SERVICE_SET_CHILD_LOCK,
SERVICE_SET_CLEAN,
SERVICE_SET_DRY,
SERVICE_SET_POWER_MODE,
SERVICE_SET_POWER_PRICE,
SERVICE_SET_WIFI_LED_OFF,
SERVICE_SET_WIFI_LED_ON,
SUCCESS,
)
from .device import XiaomiMiioEntity
from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
@ -83,8 +104,10 @@ ATTR_POWER_MODE = "power_mode"
ATTR_WIFI_LED = "wifi_led"
ATTR_POWER_PRICE = "power_price"
ATTR_PRICE = "price"
SUCCESS = ["ok"]
ATTR_BUZZER = "buzzer"
ATTR_CHILD_LOCK = "child_lock"
ATTR_DRY = "dry"
ATTR_CLEAN = "clean_mode"
FEATURE_SET_POWER_MODE = 1
FEATURE_SET_WIFI_LED = 2
@ -121,6 +144,62 @@ SERVICE_TO_METHOD = {
"method": "async_set_power_price",
"schema": SERVICE_SCHEMA_POWER_PRICE,
},
SERVICE_SET_BUZZER: {
"method_on": "async_set_buzzer_on",
"method_off": "async_set_buzzer_off",
},
SERVICE_SET_CHILD_LOCK: {
"method_on": "async_set_child_lock_on",
"method_off": "async_set_child_lock_off",
},
SERVICE_SET_DRY: {
"method_on": "async_set_dry_on",
"method_off": "async_set_dry_off",
},
SERVICE_SET_CLEAN: {
"method_on": "async_set_clean_on",
"method_off": "async_set_clean_off",
},
}
@dataclass
class SwitchType:
"""Class that holds device specific info for a xiaomi aqara or humidifiers."""
name: str = None
short_name: str = None
icon: str = None
service: str = None
available_with_device_off: bool = True
SWITCH_TYPES = {
FEATURE_SET_BUZZER: SwitchType(
name="Buzzer",
icon="mdi:volume-high",
short_name=ATTR_BUZZER,
service=SERVICE_SET_BUZZER,
),
FEATURE_SET_CHILD_LOCK: SwitchType(
name="Child Lock",
icon="mdi:lock",
short_name=ATTR_CHILD_LOCK,
service=SERVICE_SET_CHILD_LOCK,
),
FEATURE_SET_DRY: SwitchType(
name="Dry Mode",
icon="mdi:hair-dryer",
short_name=ATTR_DRY,
service=SERVICE_SET_DRY,
),
FEATURE_SET_CLEAN: SwitchType(
name="Clean Mode",
icon="mdi:sparkles",
short_name=ATTR_CLEAN,
service=SERVICE_SET_CLEAN,
available_with_device_off=False,
),
}
@ -140,14 +219,63 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the switch from a config entry."""
entities = []
if (
config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY
or config_entry.data[CONF_MODEL] == "lumi.acpartner.v3"
):
await async_setup_other_entry(hass, config_entry, async_add_entities)
else:
await async_setup_coordinated_entry(hass, config_entry, async_add_entities)
async def async_setup_coordinated_entry(hass, config_entry, async_add_entities):
"""Set up the coordinated switch from a config entry."""
entities = []
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
else:
name = config_entry.title
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
device_features = 0
if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
elif model in [MODEL_AIRHUMIDIFIER_CA4]:
device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4
elif model in MODELS_HUMIDIFIER:
device_features = FEATURE_FLAGS_AIRHUMIDIFIER
for feature, switch in SWITCH_TYPES.items():
if feature & device_features:
entities.append(
XiaomiGenericCoordinatedSwitch(
f"{name} {switch.name}",
device,
config_entry,
f"{switch.short_name}_{unique_id}",
switch,
coordinator,
)
)
async_add_entities(entities)
async def async_setup_other_entry(hass, config_entry, async_add_entities):
"""Set up the other type switch from a config entry."""
entities = []
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
# Gateway sub devices
@ -256,7 +384,131 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
DOMAIN, plug_service, async_service_handler, schema=schema
)
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)
class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity):
"""Representation of a Xiaomi Plug Generic."""
def __init__(self, name, device, entry, unique_id, switch, coordinator):
"""Initialize the plug switch."""
super().__init__(name, device, entry, unique_id, coordinator)
self._attr_icon = switch.icon
self._controller = switch
self._attr_is_on = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
# On state change the device doesn't provide the new state immediately.
self._attr_is_on = self._extract_value_from_attribute(
self.coordinator.data, self._controller.short_name
)
self.async_write_ha_state()
@property
def available(self):
"""Return true when state is known."""
if (
super().available
and not self.coordinator.data.is_on
and not self._controller.available_with_device_off
):
return False
return super().available
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def async_turn_on(self, **kwargs) -> None:
"""Turn on an option of the miio device."""
method = getattr(self, SERVICE_TO_METHOD[self._controller.service]["method_on"])
if await method():
# Write state back to avoid switch flips with a slow response
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
"""Turn off an option of the miio device."""
method = getattr(
self, SERVICE_TO_METHOD[self._controller.service]["method_off"]
)
if await method():
# Write state back to avoid switch flips with a slow response
self._attr_is_on = False
self.async_write_ha_state()
async def async_set_buzzer_on(self) -> bool:
"""Turn the buzzer on."""
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) -> bool:
"""Turn the buzzer off."""
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) -> bool:
"""Turn the child lock on."""
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) -> bool:
"""Turn the child lock off."""
return await self._try_command(
"Turning the child lock of the miio device off failed.",
self._device.set_child_lock,
False,
)
async def async_set_dry_on(self) -> bool:
"""Turn the dry mode on."""
return await self._try_command(
"Turning the dry mode of the miio device on failed.",
self._device.set_dry,
True,
)
async def async_set_dry_off(self) -> bool:
"""Turn the dry mode off."""
return await self._try_command(
"Turning the dry mode of the miio device off failed.",
self._device.set_dry,
False,
)
async def async_set_clean_on(self) -> bool:
"""Turn the dry mode on."""
return await self._try_command(
"Turning the clean mode of the miio device on failed.",
self._device.set_clean_mode,
True,
)
async def async_set_clean_off(self) -> bool:
"""Turn the dry mode off."""
return await self._try_command(
"Turning the clean mode of the miio device off failed.",
self._device.set_clean_mode,
False,
)
class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity):