mirror of https://github.com/home-assistant/core
419 lines
13 KiB
Python
419 lines
13 KiB
Python
"""Support for vacuum cleaner robots (botvacs)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from enum import IntFlag
|
|
from functools import partial
|
|
import logging
|
|
from typing import Any
|
|
|
|
from propcache import cached_property
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
|
|
ATTR_BATTERY_LEVEL,
|
|
ATTR_COMMAND,
|
|
SERVICE_TOGGLE,
|
|
SERVICE_TURN_OFF,
|
|
SERVICE_TURN_ON,
|
|
STATE_IDLE,
|
|
STATE_ON,
|
|
STATE_PAUSED,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.deprecation import (
|
|
DeprecatedConstantEnum,
|
|
all_with_deprecated_constants,
|
|
check_if_deprecated_constant,
|
|
dir_with_deprecated_constants,
|
|
)
|
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.helpers.icon import icon_for_battery_level
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.loader import bind_hass
|
|
from homeassistant.util.hass_dict import HassKey
|
|
|
|
from .const import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETURNING
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DATA_COMPONENT: HassKey[EntityComponent[StateVacuumEntity]] = HassKey(DOMAIN)
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
|
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
|
SCAN_INTERVAL = timedelta(seconds=20)
|
|
|
|
ATTR_BATTERY_ICON = "battery_icon"
|
|
ATTR_CLEANED_AREA = "cleaned_area"
|
|
ATTR_FAN_SPEED = "fan_speed"
|
|
ATTR_FAN_SPEED_LIST = "fan_speed_list"
|
|
ATTR_PARAMS = "params"
|
|
ATTR_STATUS = "status"
|
|
|
|
SERVICE_CLEAN_SPOT = "clean_spot"
|
|
SERVICE_LOCATE = "locate"
|
|
SERVICE_RETURN_TO_BASE = "return_to_base"
|
|
SERVICE_SEND_COMMAND = "send_command"
|
|
SERVICE_SET_FAN_SPEED = "set_fan_speed"
|
|
SERVICE_START_PAUSE = "start_pause"
|
|
SERVICE_START = "start"
|
|
SERVICE_PAUSE = "pause"
|
|
SERVICE_STOP = "stop"
|
|
|
|
|
|
STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR]
|
|
|
|
DEFAULT_NAME = "Vacuum cleaner robot"
|
|
|
|
|
|
class VacuumEntityFeature(IntFlag):
|
|
"""Supported features of the vacuum entity."""
|
|
|
|
TURN_ON = 1 # Deprecated, not supported by StateVacuumEntity
|
|
TURN_OFF = 2 # Deprecated, not supported by StateVacuumEntity
|
|
PAUSE = 4
|
|
STOP = 8
|
|
RETURN_HOME = 16
|
|
FAN_SPEED = 32
|
|
BATTERY = 64
|
|
STATUS = 128 # Deprecated, not supported by StateVacuumEntity
|
|
SEND_COMMAND = 256
|
|
LOCATE = 512
|
|
CLEAN_SPOT = 1024
|
|
MAP = 2048
|
|
STATE = 4096 # Must be set by vacuum platforms derived from StateVacuumEntity
|
|
START = 8192
|
|
|
|
|
|
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
|
# Please use the VacuumEntityFeature enum instead.
|
|
_DEPRECATED_SUPPORT_TURN_ON = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.TURN_ON, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_TURN_OFF = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.TURN_OFF, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_PAUSE = DeprecatedConstantEnum(VacuumEntityFeature.PAUSE, "2025.10")
|
|
_DEPRECATED_SUPPORT_STOP = DeprecatedConstantEnum(VacuumEntityFeature.STOP, "2025.10")
|
|
_DEPRECATED_SUPPORT_RETURN_HOME = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.RETURN_HOME, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_FAN_SPEED = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.FAN_SPEED, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_BATTERY = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.BATTERY, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_STATUS = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.STATUS, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_SEND_COMMAND = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.SEND_COMMAND, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_LOCATE = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.LOCATE, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_CLEAN_SPOT = DeprecatedConstantEnum(
|
|
VacuumEntityFeature.CLEAN_SPOT, "2025.10"
|
|
)
|
|
_DEPRECATED_SUPPORT_MAP = DeprecatedConstantEnum(VacuumEntityFeature.MAP, "2025.10")
|
|
_DEPRECATED_SUPPORT_STATE = DeprecatedConstantEnum(VacuumEntityFeature.STATE, "2025.10")
|
|
_DEPRECATED_SUPPORT_START = DeprecatedConstantEnum(VacuumEntityFeature.START, "2025.10")
|
|
|
|
# mypy: disallow-any-generics
|
|
|
|
|
|
@bind_hass
|
|
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
|
"""Return if the vacuum is on based on the statemachine."""
|
|
return hass.states.is_state(entity_id, STATE_ON)
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up the vacuum component."""
|
|
component = hass.data[DATA_COMPONENT] = EntityComponent[StateVacuumEntity](
|
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
|
)
|
|
|
|
await component.async_setup(config)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_START,
|
|
None,
|
|
"async_start",
|
|
[VacuumEntityFeature.START],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_PAUSE,
|
|
None,
|
|
"async_pause",
|
|
[VacuumEntityFeature.PAUSE],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_RETURN_TO_BASE,
|
|
None,
|
|
"async_return_to_base",
|
|
[VacuumEntityFeature.RETURN_HOME],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_CLEAN_SPOT,
|
|
None,
|
|
"async_clean_spot",
|
|
[VacuumEntityFeature.CLEAN_SPOT],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_LOCATE,
|
|
None,
|
|
"async_locate",
|
|
[VacuumEntityFeature.LOCATE],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_STOP,
|
|
None,
|
|
"async_stop",
|
|
[VacuumEntityFeature.STOP],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_SET_FAN_SPEED,
|
|
{vol.Required(ATTR_FAN_SPEED): cv.string},
|
|
"async_set_fan_speed",
|
|
[VacuumEntityFeature.FAN_SPEED],
|
|
)
|
|
component.async_register_entity_service(
|
|
SERVICE_SEND_COMMAND,
|
|
{
|
|
vol.Required(ATTR_COMMAND): cv.string,
|
|
vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list),
|
|
},
|
|
"async_send_command",
|
|
[VacuumEntityFeature.SEND_COMMAND],
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up a config entry."""
|
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
|
|
|
|
|
class StateVacuumEntityDescription(EntityDescription, frozen_or_thawed=True):
|
|
"""A class that describes vacuum entities."""
|
|
|
|
|
|
STATE_VACUUM_CACHED_PROPERTIES_WITH_ATTR_ = {
|
|
"supported_features",
|
|
"battery_level",
|
|
"battery_icon",
|
|
"fan_speed",
|
|
"fan_speed_list",
|
|
"state",
|
|
}
|
|
|
|
|
|
class StateVacuumEntity(
|
|
Entity, cached_properties=STATE_VACUUM_CACHED_PROPERTIES_WITH_ATTR_
|
|
):
|
|
"""Representation of a vacuum cleaner robot that supports states."""
|
|
|
|
entity_description: StateVacuumEntityDescription
|
|
|
|
_entity_component_unrecorded_attributes = frozenset({ATTR_FAN_SPEED_LIST})
|
|
|
|
_attr_battery_icon: str
|
|
_attr_battery_level: int | None = None
|
|
_attr_fan_speed: str | None = None
|
|
_attr_fan_speed_list: list[str]
|
|
_attr_state: str | None = None
|
|
_attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
|
|
|
|
@cached_property
|
|
def battery_level(self) -> int | None:
|
|
"""Return the battery level of the vacuum cleaner."""
|
|
return self._attr_battery_level
|
|
|
|
@property
|
|
def battery_icon(self) -> str:
|
|
"""Return the battery icon for the vacuum cleaner."""
|
|
charging = bool(self.state == STATE_DOCKED)
|
|
|
|
return icon_for_battery_level(
|
|
battery_level=self.battery_level, charging=charging
|
|
)
|
|
|
|
@property
|
|
def capability_attributes(self) -> dict[str, Any] | None:
|
|
"""Return capability attributes."""
|
|
if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat:
|
|
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
|
|
return None
|
|
|
|
@cached_property
|
|
def fan_speed(self) -> str | None:
|
|
"""Return the fan speed of the vacuum cleaner."""
|
|
return self._attr_fan_speed
|
|
|
|
@cached_property
|
|
def fan_speed_list(self) -> list[str]:
|
|
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
|
return self._attr_fan_speed_list
|
|
|
|
@property
|
|
def state_attributes(self) -> dict[str, Any]:
|
|
"""Return the state attributes of the vacuum cleaner."""
|
|
data: dict[str, Any] = {}
|
|
supported_features = self.supported_features_compat
|
|
|
|
if VacuumEntityFeature.BATTERY in supported_features:
|
|
data[ATTR_BATTERY_LEVEL] = self.battery_level
|
|
data[ATTR_BATTERY_ICON] = self.battery_icon
|
|
|
|
if VacuumEntityFeature.FAN_SPEED in supported_features:
|
|
data[ATTR_FAN_SPEED] = self.fan_speed
|
|
|
|
return data
|
|
|
|
@cached_property
|
|
def state(self) -> str | None:
|
|
"""Return the state of the vacuum cleaner."""
|
|
return self._attr_state
|
|
|
|
@cached_property
|
|
def supported_features(self) -> VacuumEntityFeature:
|
|
"""Flag vacuum cleaner features that are supported."""
|
|
return self._attr_supported_features
|
|
|
|
@property
|
|
def supported_features_compat(self) -> VacuumEntityFeature:
|
|
"""Return the supported features as VacuumEntityFeature.
|
|
|
|
Remove this compatibility shim in 2025.1 or later.
|
|
"""
|
|
features = self.supported_features
|
|
if type(features) is int: # noqa: E721
|
|
new_features = VacuumEntityFeature(features)
|
|
self._report_deprecated_supported_features_values(new_features)
|
|
return new_features
|
|
return features
|
|
|
|
def stop(self, **kwargs: Any) -> None:
|
|
"""Stop the vacuum cleaner."""
|
|
raise NotImplementedError
|
|
|
|
async def async_stop(self, **kwargs: Any) -> None:
|
|
"""Stop the vacuum cleaner.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(partial(self.stop, **kwargs))
|
|
|
|
def return_to_base(self, **kwargs: Any) -> None:
|
|
"""Set the vacuum cleaner to return to the dock."""
|
|
raise NotImplementedError
|
|
|
|
async def async_return_to_base(self, **kwargs: Any) -> None:
|
|
"""Set the vacuum cleaner to return to the dock.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(partial(self.return_to_base, **kwargs))
|
|
|
|
def clean_spot(self, **kwargs: Any) -> None:
|
|
"""Perform a spot clean-up."""
|
|
raise NotImplementedError
|
|
|
|
async def async_clean_spot(self, **kwargs: Any) -> None:
|
|
"""Perform a spot clean-up.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(partial(self.clean_spot, **kwargs))
|
|
|
|
def locate(self, **kwargs: Any) -> None:
|
|
"""Locate the vacuum cleaner."""
|
|
raise NotImplementedError
|
|
|
|
async def async_locate(self, **kwargs: Any) -> None:
|
|
"""Locate the vacuum cleaner.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(partial(self.locate, **kwargs))
|
|
|
|
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
|
"""Set fan speed."""
|
|
raise NotImplementedError
|
|
|
|
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
|
"""Set fan speed.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(
|
|
partial(self.set_fan_speed, fan_speed, **kwargs)
|
|
)
|
|
|
|
def send_command(
|
|
self,
|
|
command: str,
|
|
params: dict[str, Any] | list[Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Send a command to a vacuum cleaner."""
|
|
raise NotImplementedError
|
|
|
|
async def async_send_command(
|
|
self,
|
|
command: str,
|
|
params: dict[str, Any] | list[Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Send a command to a vacuum cleaner.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(
|
|
partial(self.send_command, command, params=params, **kwargs)
|
|
)
|
|
|
|
def start(self) -> None:
|
|
"""Start or resume the cleaning task."""
|
|
raise NotImplementedError
|
|
|
|
async def async_start(self) -> None:
|
|
"""Start or resume the cleaning task.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(self.start)
|
|
|
|
def pause(self) -> None:
|
|
"""Pause the cleaning task."""
|
|
raise NotImplementedError
|
|
|
|
async def async_pause(self) -> None:
|
|
"""Pause the cleaning task.
|
|
|
|
This method must be run in the event loop.
|
|
"""
|
|
await self.hass.async_add_executor_job(self.pause)
|
|
|
|
|
|
# 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())
|