core/homeassistant/components/xiaomi_miio/humidifier.py

440 lines
15 KiB
Python

"""Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity."""
import logging
import math
from typing import Any
from miio.integrations.humidifier.deerma.airhumidifier_mjjsq import (
OperationMode as AirhumidifierMjjsqOperationMode,
)
from miio.integrations.humidifier.zhimi.airhumidifier import (
OperationMode as AirhumidifierOperationMode,
)
from miio.integrations.humidifier.zhimi.airhumidifier_miot import (
OperationMode as AirhumidifierMiotOperationMode,
)
from homeassistant.components.humidifier import (
ATTR_HUMIDITY,
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import percentage_to_ranged_value
from .const import (
CONF_FLOW_TYPE,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODELS_HUMIDIFIER_MIOT,
MODELS_HUMIDIFIER_MJJSQ,
)
from .entity import XiaomiCoordinatedMiioEntity
_LOGGER = logging.getLogger(__name__)
# Air Humidifier
ATTR_TARGET_HUMIDITY = "target_humidity"
AVAILABLE_ATTRIBUTES = {
ATTR_MODE: "mode",
ATTR_TARGET_HUMIDITY: "target_humidity",
ATTR_HUMIDITY: "humidity",
}
AVAILABLE_MODES_CA1_CB1 = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Strong
]
AVAILABLE_MODES_CA4 = [mode.name for mode in AirhumidifierMiotOperationMode]
AVAILABLE_MODES_MJJSQ = [
mode.name
for mode in AirhumidifierMjjsqOperationMode
if mode is not AirhumidifierMjjsqOperationMode.WetAndProtect
]
AVAILABLE_MODES_OTHER = [
mode.name
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Auto
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Humidifier from a config entry."""
if config_entry.data[CONF_FLOW_TYPE] != CONF_DEVICE:
return
entities: list[HumidifierEntity] = []
entity: HumidifierEntity
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
if model in MODELS_HUMIDIFIER_MIOT:
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiAirHumidifierMiot(
air_humidifier,
config_entry,
unique_id,
coordinator,
)
elif model in MODELS_HUMIDIFIER_MJJSQ:
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiAirHumidifierMjjsq(
air_humidifier,
config_entry,
unique_id,
coordinator,
)
else:
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiAirHumidifier(
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 = HumidifierDeviceClass.HUMIDIFIER
_attr_supported_features = HumidifierEntityFeature.MODES
_attr_name = None
def __init__(self, device, entry, unique_id, coordinator):
"""Initialize the generic Xiaomi device."""
super().__init__(device, entry, unique_id, coordinator=coordinator)
self._state = None
self._attributes = {}
self._mode = None
self._humidity_steps = 100
self._target_humidity = None
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def mode(self):
"""Get the current mode."""
return self._mode
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
result = await self._try_command(
"Turning the miio device on failed.", self._device.on
)
if result:
self._state = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> 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.async_write_ha_state()
def translate_humidity(self, humidity: float) -> float | None:
"""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."""
available_modes: list[str]
def __init__(self, device, entry, unique_id, coordinator):
"""Initialize the plug switch."""
super().__init__(device, entry, unique_id, coordinator)
self._attr_min_humidity = 30
self._attr_max_humidity = 80
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._attr_available_modes = AVAILABLE_MODES_CA1_CB1
self._humidity_steps = 10
elif self._model in [MODEL_AIRHUMIDIFIER_CA4]:
self._attr_available_modes = AVAILABLE_MODES_CA4
self._humidity_steps = 100
elif self._model in MODELS_HUMIDIFIER_MJJSQ:
self._attr_available_modes = AVAILABLE_MODES_MJJSQ
self._humidity_steps = 100
else:
self._attr_available_modes = AVAILABLE_MODES_OTHER
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._attr_current_humidity = self._attributes[ATTR_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._attr_current_humidity = self._attributes[ATTR_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.value
or AirhumidifierOperationMode.Auto.name not in self.available_modes
else None
)
async def async_set_humidity(self, humidity: float) -> 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 & HumidifierEntityFeature.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.value
self.async_write_ha_state()
async def async_set_mode(self, mode: str) -> None:
"""Set the mode of the humidifier."""
if self.supported_features & HumidifierEntityFeature.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],
):
self._mode = mode.lower()
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: float) -> 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 & HumidifierEntityFeature.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 & HumidifierEntityFeature.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()
class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier):
"""Representation of a Xiaomi Air MJJSQ Humidifier."""
MODE_MAPPING = {
"Low": AirhumidifierMjjsqOperationMode.Low,
"Medium": AirhumidifierMjjsqOperationMode.Medium,
"High": AirhumidifierMjjsqOperationMode.High,
"Humidity": AirhumidifierMjjsqOperationMode.Humidity,
}
@property
def mode(self):
"""Return the current mode."""
return AirhumidifierMjjsqOperationMode(self._mode).name
@property
def target_humidity(self):
"""Return the target humidity."""
if self._state:
if (
AirhumidifierMjjsqOperationMode(self._mode)
== AirhumidifierMjjsqOperationMode.Humidity
):
return self._target_humidity
return None
async def async_set_humidity(self, humidity: float) -> None:
"""Set the target humidity of the humidifier and set the mode to Humidity."""
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 & HumidifierEntityFeature.MODES == 0
or AirhumidifierMjjsqOperationMode(self._attributes[ATTR_MODE])
== AirhumidifierMjjsqOperationMode.Humidity
):
self.async_write_ha_state()
return
_LOGGER.debug("Setting the operation mode to: Humidity")
if await self._try_command(
"Setting operation mode of the miio device to MODE_HUMIDITY failed.",
self._device.set_mode,
AirhumidifierMjjsqOperationMode.Humidity,
):
self._mode = 3
self.async_write_ha_state()
async def async_set_mode(self, mode: str) -> None:
"""Set the mode of the fan."""
if mode not in self.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.MODE_MAPPING[mode],
):
self._mode = self.MODE_MAPPING[mode].value
self.async_write_ha_state()