ha-core/homeassistant/components/humidifier/__init__.py

296 lines
9.1 KiB
Python

"""Provides functionality to interact with humidifier devices."""
from __future__ import annotations
from datetime import timedelta
from enum import StrEnum
from functools import cached_property, partial
import logging
from typing import Any, final
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_MODE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.deprecation import (
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .const import ( # noqa: F401
_DEPRECATED_DEVICE_CLASS_DEHUMIDIFIER,
_DEPRECATED_DEVICE_CLASS_HUMIDIFIER,
_DEPRECATED_SUPPORT_MODES,
ATTR_ACTION,
ATTR_AVAILABLE_MODES,
ATTR_CURRENT_HUMIDITY,
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DOMAIN,
MODE_AUTO,
MODE_AWAY,
MODE_NORMAL,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
HumidifierAction,
HumidifierEntityFeature,
)
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
class HumidifierDeviceClass(StrEnum):
"""Device class for humidifiers."""
HUMIDIFIER = "humidifier"
DEHUMIDIFIER = "dehumidifier"
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(HumidifierDeviceClass))
# DEVICE_CLASSES below is deprecated as of 2021.12
# use the HumidifierDeviceClass enum instead.
DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass]
# mypy: disallow-any-generics
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the humidifier is on based on the statemachine.
Async friendly.
"""
return hass.states.is_state(entity_id, STATE_ON)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up humidifier devices."""
component = hass.data[DOMAIN] = EntityComponent[HumidifierEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
component.async_register_entity_service(
SERVICE_SET_MODE,
{vol.Required(ATTR_MODE): cv.string},
"async_set_mode",
[HumidifierEntityFeature.MODES],
)
component.async_register_entity_service(
SERVICE_SET_HUMIDITY,
{
vol.Required(ATTR_HUMIDITY): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
},
"async_set_humidity",
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class HumidifierEntityDescription(ToggleEntityDescription, frozen_or_thawed=True):
"""A class that describes humidifier entities."""
device_class: HumidifierDeviceClass | None = None
CACHED_PROPERTIES_WITH_ATTR_ = {
"device_class",
"action",
"current_humidity",
"target_humidity",
"mode",
"available_modes",
"min_humidity",
"max_humidity",
"supported_features",
}
class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Base class for humidifier entities."""
_entity_component_unrecorded_attributes = frozenset(
{ATTR_MIN_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_AVAILABLE_MODES}
)
entity_description: HumidifierEntityDescription
_attr_action: HumidifierAction | None = None
_attr_available_modes: list[str] | None
_attr_current_humidity: float | None = None
_attr_device_class: HumidifierDeviceClass | None
_attr_max_humidity: float = DEFAULT_MAX_HUMIDITY
_attr_min_humidity: float = DEFAULT_MIN_HUMIDITY
_attr_mode: str | None
_attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature(0)
_attr_target_humidity: float | None = None
@property
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
data: dict[str, Any] = {
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
}
if HumidifierEntityFeature.MODES in self.supported_features_compat:
data[ATTR_AVAILABLE_MODES] = self.available_modes
return data
@cached_property
def device_class(self) -> HumidifierDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
return self._attr_device_class
if hasattr(self, "entity_description"):
return self.entity_description.device_class
return None
@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
data: dict[str, Any] = {}
if self.action is not None:
data[ATTR_ACTION] = self.action if self.is_on else HumidifierAction.OFF
if self.current_humidity is not None:
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
if self.target_humidity is not None:
data[ATTR_HUMIDITY] = self.target_humidity
if HumidifierEntityFeature.MODES in self.supported_features_compat:
data[ATTR_MODE] = self.mode
return data
@cached_property
def action(self) -> HumidifierAction | None:
"""Return the current action."""
return self._attr_action
@cached_property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
return self._attr_current_humidity
@cached_property
def target_humidity(self) -> float | None:
"""Return the humidity we try to reach."""
return self._attr_target_humidity
@cached_property
def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby.
Requires HumidifierEntityFeature.MODES.
"""
return self._attr_mode
@cached_property
def available_modes(self) -> list[str] | None:
"""Return a list of available modes.
Requires HumidifierEntityFeature.MODES.
"""
return self._attr_available_modes
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
raise NotImplementedError
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self.hass.async_add_executor_job(self.set_humidity, humidity)
def set_mode(self, mode: str) -> None:
"""Set new mode."""
raise NotImplementedError
async def async_set_mode(self, mode: str) -> None:
"""Set new mode."""
await self.hass.async_add_executor_job(self.set_mode, mode)
@cached_property
def min_humidity(self) -> float:
"""Return the minimum humidity."""
return self._attr_min_humidity
@cached_property
def max_humidity(self) -> float:
"""Return the maximum humidity."""
return self._attr_max_humidity
@cached_property
def supported_features(self) -> HumidifierEntityFeature:
"""Return the list of supported features."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> HumidifierEntityFeature:
"""Return the supported features as HumidifierEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = HumidifierEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
# As we import deprecated constants from the const module, we need to add these two functions
# otherwise this module will be logged for using deprecated constants and not the custom component
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())