Implement new state property for vacuum which is using an enum (#126353)

* Implement new state property for vacuum which is using an enum

* Mod

* Mod init

* Mods

* Fix integrations

* Tests

* Fix state

* Add vacuum tests

* Fix last test

* Litterrobot tests

* Fixes

* Tests

* Fixes

* Fix VacuumEntity

* Mods

* Mods

* Mods

* Update demo

* LG

* Fix vacuum

* Fix Matter

* Fix deprecation version

* Mods

* Fixes

* Fix ruff

* Fix tests

* Fix roomba

* Fix breaking dates
pull/132475/head
G Johansson 2024-12-06 11:16:03 +01:00 committed by GitHub
parent bd9aefda62
commit 2eaf206562
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 844 additions and 509 deletions

View File

@ -436,7 +436,7 @@ class AlexaPowerController(AlexaCapability):
elif self.entity.domain == remote.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING
is_on = self.entity.state == vacuum.VacuumActivity.CLEANING
elif self.entity.domain == timer.DOMAIN:
is_on = self.entity.state != STATE_IDLE
elif self.entity.domain == water_heater.DOMAIN:

View File

@ -7,12 +7,8 @@ from typing import Any
from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA,
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@ -91,16 +87,11 @@ class StateDemoVacuum(StateVacuumEntity):
"""Initialize the vacuum."""
self._attr_name = name
self._attr_supported_features = supported_features
self._state = STATE_DOCKED
self._attr_activity = VacuumActivity.DOCKED
self._fan_speed = FAN_SPEEDS[1]
self._cleaned_area: float = 0
self._battery_level = 100
@property
def state(self) -> str:
"""Return the current state of the vacuum."""
return self._state
@property
def battery_level(self) -> int:
"""Return the current battery level of the vacuum."""
@ -123,33 +114,33 @@ class StateDemoVacuum(StateVacuumEntity):
def start(self) -> None:
"""Start or resume the cleaning task."""
if self._state != STATE_CLEANING:
self._state = STATE_CLEANING
if self._attr_activity != VacuumActivity.CLEANING:
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def pause(self) -> None:
"""Pause the cleaning task."""
if self._state == STATE_CLEANING:
self._state = STATE_PAUSED
if self._attr_activity == VacuumActivity.CLEANING:
self._attr_activity = VacuumActivity.PAUSED
self.schedule_update_ha_state()
def stop(self, **kwargs: Any) -> None:
"""Stop the cleaning task, do not return to dock."""
self._state = STATE_IDLE
self._attr_activity = VacuumActivity.IDLE
self.schedule_update_ha_state()
def return_to_base(self, **kwargs: Any) -> None:
"""Return dock to charging base."""
self._state = STATE_RETURNING
self._attr_activity = VacuumActivity.RETURNING
self.schedule_update_ha_state()
event.call_later(self.hass, 30, self.__set_state_to_dock)
def clean_spot(self, **kwargs: Any) -> None:
"""Perform a spot clean-up."""
self._state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
@ -167,12 +158,12 @@ class StateDemoVacuum(StateVacuumEntity):
"persistent_notification",
service_data={"message": "I'm here!", "title": "Locate request"},
)
self._state = STATE_IDLE
self._attr_activity = VacuumActivity.IDLE
self.async_write_ha_state()
async def async_clean_spot(self, **kwargs: Any) -> None:
"""Locate the vacuum's position."""
self._state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
self.async_write_ha_state()
async def async_send_command(
@ -182,9 +173,9 @@ class StateDemoVacuum(StateVacuumEntity):
**kwargs: Any,
) -> None:
"""Send a command to the vacuum."""
self._state = STATE_IDLE
self._attr_activity = VacuumActivity.IDLE
self.async_write_ha_state()
def __set_state_to_dock(self, _: datetime) -> None:
self._state = STATE_DOCKED
self._attr_activity = VacuumActivity.DOCKED
self.schedule_update_ha_state()

View File

@ -13,14 +13,9 @@ from deebot_client.models import CleanAction, CleanMode, Room, State
import sucks
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
StateVacuumEntityDescription,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant, SupportsResponse
@ -123,22 +118,22 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
self.schedule_update_ha_state()
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Return the state of the vacuum cleaner."""
if self.error is not None:
return STATE_ERROR
return VacuumActivity.ERROR
if self.device.is_cleaning:
return STATE_CLEANING
return VacuumActivity.CLEANING
if self.device.is_charging:
return STATE_DOCKED
return VacuumActivity.DOCKED
if self.device.vacuum_status == sucks.CLEAN_MODE_STOP:
return STATE_IDLE
return VacuumActivity.IDLE
if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING:
return STATE_RETURNING
return VacuumActivity.RETURNING
return None
@ -202,7 +197,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set fan speed."""
if self.state == STATE_CLEANING:
if self.state == VacuumActivity.CLEANING:
self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed))
def send_command(
@ -225,12 +220,12 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
_STATE_TO_VACUUM_STATE = {
State.IDLE: STATE_IDLE,
State.CLEANING: STATE_CLEANING,
State.RETURNING: STATE_RETURNING,
State.DOCKED: STATE_DOCKED,
State.ERROR: STATE_ERROR,
State.PAUSED: STATE_PAUSED,
State.IDLE: VacuumActivity.IDLE,
State.CLEANING: VacuumActivity.CLEANING,
State.RETURNING: VacuumActivity.RETURNING,
State.DOCKED: VacuumActivity.DOCKED,
State.ERROR: VacuumActivity.ERROR,
State.PAUSED: VacuumActivity.PAUSED,
}
_ATTR_ROOMS = "rooms"
@ -284,7 +279,7 @@ class EcovacsVacuum(
self.async_write_ha_state()
async def on_status(event: StateEvent) -> None:
self._attr_state = _STATE_TO_VACUUM_STATE[event.state]
self._attr_activity = _STATE_TO_VACUUM_STATE[event.state]
self.async_write_ha_state()
self._subscribe(self._capability.battery.event, on_battery)

View File

@ -729,7 +729,7 @@ class DockTrait(_Trait):
def query_attributes(self) -> dict[str, Any]:
"""Return dock query attributes."""
return {"isDocked": self.state.state == vacuum.STATE_DOCKED}
return {"isDocked": self.state.state == vacuum.VacuumActivity.DOCKED}
async def execute(self, command, data, params, challenge):
"""Execute a dock command."""
@ -825,8 +825,8 @@ class EnergyStorageTrait(_Trait):
"capacityUntilFull": [
{"rawValue": 100 - battery_level, "unit": "PERCENTAGE"}
],
"isCharging": self.state.state == vacuum.STATE_DOCKED,
"isPluggedIn": self.state.state == vacuum.STATE_DOCKED,
"isCharging": self.state.state == vacuum.VacuumActivity.DOCKED,
"isPluggedIn": self.state.state == vacuum.VacuumActivity.DOCKED,
}
async def execute(self, command, data, params, challenge):
@ -882,8 +882,8 @@ class StartStopTrait(_Trait):
if domain == vacuum.DOMAIN:
return {
"isRunning": state == vacuum.STATE_CLEANING,
"isPaused": state == vacuum.STATE_PAUSED,
"isRunning": state == vacuum.VacuumActivity.CLEANING,
"isPaused": state == vacuum.VacuumActivity.PAUSED,
}
if domain in COVER_VALVE_DOMAINS:

View File

@ -11,7 +11,7 @@ from typing import Protocol
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.climate import HVACMode
from homeassistant.components.lock import LockState
from homeassistant.components.vacuum import STATE_CLEANING, STATE_ERROR, STATE_RETURNING
from homeassistant.components.vacuum import VacuumActivity
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_ELECTRIC,
@ -105,9 +105,9 @@ ON_OFF_STATES: dict[Platform | str, tuple[set[str], str, str]] = {
Platform.VACUUM: (
{
STATE_ON,
STATE_CLEANING,
STATE_RETURNING,
STATE_ERROR,
VacuumActivity.CLEANING,
VacuumActivity.RETURNING,
VacuumActivity.ERROR,
},
STATE_ON,
STATE_OFF,

View File

@ -21,7 +21,7 @@ from homeassistant.components.vacuum import (
DOMAIN as VACUUM_DOMAIN,
SERVICE_RETURN_TO_BASE,
SERVICE_START,
STATE_CLEANING,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import (
@ -213,7 +213,7 @@ class Vacuum(Switch):
@callback
def async_update_state(self, new_state: State) -> None:
"""Update switch state after state changed."""
current_state = new_state.state in (STATE_CLEANING, STATE_ON)
current_state = new_state.state in (VacuumActivity.CLEANING, STATE_ON)
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
self.char_on.set_value(current_state)

View File

@ -9,15 +9,11 @@ from thinqconnect import DeviceType
from thinqconnect.integration import ExtendedProperty
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_RETURNING,
StateVacuumEntity,
StateVacuumEntityDescription,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import STATE_IDLE, STATE_PAUSED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -46,21 +42,21 @@ class State(StrEnum):
ROBOT_STATUS_TO_HA = {
"charging": STATE_DOCKED,
"diagnosis": STATE_IDLE,
"homing": STATE_RETURNING,
"initializing": STATE_IDLE,
"macrosector": STATE_IDLE,
"monitoring_detecting": STATE_IDLE,
"monitoring_moving": STATE_IDLE,
"monitoring_positioning": STATE_IDLE,
"pause": STATE_PAUSED,
"reservation": STATE_IDLE,
"setdate": STATE_IDLE,
"sleep": STATE_IDLE,
"standby": STATE_IDLE,
"working": STATE_CLEANING,
"error": STATE_ERROR,
"charging": VacuumActivity.DOCKED,
"diagnosis": VacuumActivity.IDLE,
"homing": VacuumActivity.RETURNING,
"initializing": VacuumActivity.IDLE,
"macrosector": VacuumActivity.IDLE,
"monitoring_detecting": VacuumActivity.IDLE,
"monitoring_moving": VacuumActivity.IDLE,
"monitoring_positioning": VacuumActivity.IDLE,
"pause": VacuumActivity.PAUSED,
"reservation": VacuumActivity.IDLE,
"setdate": VacuumActivity.IDLE,
"sleep": VacuumActivity.IDLE,
"standby": VacuumActivity.IDLE,
"working": VacuumActivity.CLEANING,
"error": VacuumActivity.ERROR,
}
ROBOT_BATT_TO_HA = {
"moveless": 5,
@ -114,7 +110,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
super()._update_status()
# Update state.
self._attr_state = ROBOT_STATUS_TO_HA[self.data.current_state]
self._attr_activity = ROBOT_STATUS_TO_HA[self.data.current_state]
# Update battery.
if (level := self.data.battery) is not None:
@ -135,7 +131,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
"""Start the device."""
if self.data.current_state == State.SLEEP:
value = State.WAKE_UP
elif self._attr_state == STATE_PAUSED:
elif self._attr_activity == VacuumActivity.PAUSED:
value = State.RESUME
else:
value = State.START

View File

@ -10,12 +10,9 @@ from pylitterbot.enums import LitterBoxStatus
import voluptuous as vol
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_PAUSED,
StateVacuumEntity,
StateVacuumEntityDescription,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant
@ -29,16 +26,16 @@ from .entity import LitterRobotEntity
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
LITTER_BOX_STATUS_STATE_MAP = {
LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING,
LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING,
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
LitterBoxStatus.CAT_DETECTED: STATE_DOCKED,
LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED,
LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED,
LitterBoxStatus.READY: STATE_DOCKED,
LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED,
LitterBoxStatus.OFF: STATE_DOCKED,
LitterBoxStatus.CLEAN_CYCLE: VacuumActivity.CLEANING,
LitterBoxStatus.EMPTY_CYCLE: VacuumActivity.CLEANING,
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumActivity.DOCKED,
LitterBoxStatus.CAT_DETECTED: VacuumActivity.DOCKED,
LitterBoxStatus.CAT_SENSOR_TIMING: VacuumActivity.DOCKED,
LitterBoxStatus.DRAWER_FULL_1: VacuumActivity.DOCKED,
LitterBoxStatus.DRAWER_FULL_2: VacuumActivity.DOCKED,
LitterBoxStatus.READY: VacuumActivity.DOCKED,
LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumActivity.PAUSED,
LitterBoxStatus.OFF: VacuumActivity.DOCKED,
}
LITTER_BOX_ENTITY = StateVacuumEntityDescription(
@ -78,9 +75,9 @@ class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity):
)
@property
def state(self) -> str:
def activity(self) -> VacuumActivity:
"""Return the state of the cleaner."""
return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, STATE_ERROR)
return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, VacuumActivity.ERROR)
@property
def status(self) -> str:

View File

@ -9,16 +9,13 @@ from chip.clusters import Objects as clusters
from matter_server.client.models import device_types
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_RETURNING,
StateVacuumEntity,
StateVacuumEntityDescription,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -127,25 +124,25 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
operational_state: int = self.get_matter_attribute_value(
clusters.RvcOperationalState.Attributes.OperationalState
)
state: str | None = None
state: VacuumActivity | None = None
if TYPE_CHECKING:
assert self._supported_run_modes is not None
if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
state = STATE_DOCKED
state = VacuumActivity.DOCKED
elif operational_state == OperationalState.SEEKING_CHARGER:
state = STATE_RETURNING
state = VacuumActivity.RETURNING
elif operational_state in (
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
OperationalState.UNABLE_TO_START_OR_RESUME,
):
state = STATE_ERROR
state = VacuumActivity.ERROR
elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
tags = {x.value for x in run_mode.modeTags}
if ModeTag.CLEANING in tags:
state = STATE_CLEANING
state = VacuumActivity.CLEANING
elif ModeTag.IDLE in tags:
state = STATE_IDLE
self._attr_state = state
state = VacuumActivity.IDLE
self._attr_activity = state
@callback
def _calculate_features(self) -> None:

View File

@ -10,20 +10,12 @@ import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
CONF_NAME,
STATE_IDLE,
STATE_PAUSED,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -45,13 +37,20 @@ BATTERY = "battery_level"
FAN_SPEED = "fan_speed"
STATE = "state"
POSSIBLE_STATES: dict[str, str] = {
STATE_IDLE: STATE_IDLE,
STATE_DOCKED: STATE_DOCKED,
STATE_ERROR: STATE_ERROR,
STATE_PAUSED: STATE_PAUSED,
STATE_RETURNING: STATE_RETURNING,
STATE_CLEANING: STATE_CLEANING,
STATE_IDLE = "idle"
STATE_DOCKED = "docked"
STATE_ERROR = "error"
STATE_PAUSED = "paused"
STATE_RETURNING = "returning"
STATE_CLEANING = "cleaning"
POSSIBLE_STATES: dict[str, VacuumActivity] = {
STATE_IDLE: VacuumActivity.IDLE,
STATE_DOCKED: VacuumActivity.DOCKED,
STATE_ERROR: VacuumActivity.ERROR,
STATE_PAUSED: VacuumActivity.PAUSED,
STATE_RETURNING: VacuumActivity.RETURNING,
STATE_CLEANING: VacuumActivity.CLEANING,
}
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
@ -265,7 +264,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
if STATE in payload and (
(state := payload[STATE]) in POSSIBLE_STATES or state is None
):
self._attr_state = (
self._attr_activity = (
POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None
)
del payload[STATE]
@ -277,7 +276,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
self.add_subscription(
CONF_STATE_TOPIC,
self._state_message_received,
{"_attr_battery_level", "_attr_fan_speed", "_attr_state"},
{"_attr_battery_level", "_attr_fan_speed", "_attr_activity"},
)
async def _subscribe_topics(self) -> None:

View File

@ -12,15 +12,12 @@ import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_STATUS,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE, STATE_IDLE, STATE_PAUSED
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import DeviceInfo
@ -169,23 +166,23 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
robot_alert = None
if self._state["state"] == 1:
if self._state["details"]["isCharging"]:
self._attr_state = STATE_DOCKED
self._attr_activity = VacuumActivity.DOCKED
self._status_state = "Charging"
elif (
self._state["details"]["isDocked"]
and not self._state["details"]["isCharging"]
):
self._attr_state = STATE_DOCKED
self._attr_activity = VacuumActivity.DOCKED
self._status_state = "Docked"
else:
self._attr_state = STATE_IDLE
self._attr_activity = VacuumActivity.IDLE
self._status_state = "Stopped"
if robot_alert is not None:
self._status_state = robot_alert
elif self._state["state"] == 2:
if robot_alert is None:
self._attr_state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
self._status_state = (
f"{MODE.get(self._state['cleaning']['mode'])} "
f"{ACTION.get(self._state['action'])}"
@ -200,10 +197,10 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
else:
self._status_state = robot_alert
elif self._state["state"] == 3:
self._attr_state = STATE_PAUSED
self._attr_activity = VacuumActivity.PAUSED
self._status_state = "Paused"
elif self._state["state"] == 4:
self._attr_state = STATE_ERROR
self._attr_activity = VacuumActivity.ERROR
self._status_state = ERRORS.get(self._state["error"])
self._attr_battery_level = self._state["details"]["charge"]
@ -326,9 +323,9 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
def return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock."""
try:
if self._attr_state == STATE_CLEANING:
if self._attr_activity == VacuumActivity.CLEANING:
self.robot.pause_cleaning()
self._attr_state = STATE_RETURNING
self._attr_activity = VacuumActivity.RETURNING
self.robot.send_to_base()
except NeatoRobotException as ex:
_LOGGER.error(
@ -380,7 +377,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
"Start cleaning zone '%s' with robot %s", zone, self.entity_id
)
self._attr_state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
try:
self.robot.start_cleaning(mode, navigation, category, boundary_id)
except NeatoRobotException as ex:

View File

@ -8,13 +8,8 @@ from roborock.roborock_message import RoborockDataProtocol
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
@ -27,29 +22,29 @@ from .coordinator import RoborockDataUpdateCoordinator
from .entity import RoborockCoordinatedEntityV1
STATE_CODE_TO_STATE = {
RoborockStateCode.starting: STATE_IDLE, # "Starting"
RoborockStateCode.charger_disconnected: STATE_IDLE, # "Charger disconnected"
RoborockStateCode.idle: STATE_IDLE, # "Idle"
RoborockStateCode.remote_control_active: STATE_CLEANING, # "Remote control active"
RoborockStateCode.cleaning: STATE_CLEANING, # "Cleaning"
RoborockStateCode.returning_home: STATE_RETURNING, # "Returning home"
RoborockStateCode.manual_mode: STATE_CLEANING, # "Manual mode"
RoborockStateCode.charging: STATE_DOCKED, # "Charging"
RoborockStateCode.charging_problem: STATE_ERROR, # "Charging problem"
RoborockStateCode.paused: STATE_PAUSED, # "Paused"
RoborockStateCode.spot_cleaning: STATE_CLEANING, # "Spot cleaning"
RoborockStateCode.error: STATE_ERROR, # "Error"
RoborockStateCode.shutting_down: STATE_IDLE, # "Shutting down"
RoborockStateCode.updating: STATE_DOCKED, # "Updating"
RoborockStateCode.docking: STATE_RETURNING, # "Docking"
RoborockStateCode.going_to_target: STATE_CLEANING, # "Going to target"
RoborockStateCode.zoned_cleaning: STATE_CLEANING, # "Zoned cleaning"
RoborockStateCode.segment_cleaning: STATE_CLEANING, # "Segment cleaning"
RoborockStateCode.emptying_the_bin: STATE_DOCKED, # "Emptying the bin" on s7+
RoborockStateCode.washing_the_mop: STATE_DOCKED, # "Washing the mop" on s7maxV
RoborockStateCode.going_to_wash_the_mop: STATE_RETURNING, # "Going to wash the mop" on s7maxV
RoborockStateCode.charging_complete: STATE_DOCKED, # "Charging complete"
RoborockStateCode.device_offline: STATE_ERROR, # "Device offline"
RoborockStateCode.starting: VacuumActivity.IDLE, # "Starting"
RoborockStateCode.charger_disconnected: VacuumActivity.IDLE, # "Charger disconnected"
RoborockStateCode.idle: VacuumActivity.IDLE, # "Idle"
RoborockStateCode.remote_control_active: VacuumActivity.CLEANING, # "Remote control active"
RoborockStateCode.cleaning: VacuumActivity.CLEANING, # "Cleaning"
RoborockStateCode.returning_home: VacuumActivity.RETURNING, # "Returning home"
RoborockStateCode.manual_mode: VacuumActivity.CLEANING, # "Manual mode"
RoborockStateCode.charging: VacuumActivity.DOCKED, # "Charging"
RoborockStateCode.charging_problem: VacuumActivity.ERROR, # "Charging problem"
RoborockStateCode.paused: VacuumActivity.PAUSED, # "Paused"
RoborockStateCode.spot_cleaning: VacuumActivity.CLEANING, # "Spot cleaning"
RoborockStateCode.error: VacuumActivity.ERROR, # "Error"
RoborockStateCode.shutting_down: VacuumActivity.IDLE, # "Shutting down"
RoborockStateCode.updating: VacuumActivity.DOCKED, # "Updating"
RoborockStateCode.docking: VacuumActivity.RETURNING, # "Docking"
RoborockStateCode.going_to_target: VacuumActivity.CLEANING, # "Going to target"
RoborockStateCode.zoned_cleaning: VacuumActivity.CLEANING, # "Zoned cleaning"
RoborockStateCode.segment_cleaning: VacuumActivity.CLEANING, # "Segment cleaning"
RoborockStateCode.emptying_the_bin: VacuumActivity.DOCKED, # "Emptying the bin" on s7+
RoborockStateCode.washing_the_mop: VacuumActivity.DOCKED, # "Washing the mop" on s7maxV
RoborockStateCode.going_to_wash_the_mop: VacuumActivity.RETURNING, # "Going to wash the mop" on s7maxV
RoborockStateCode.charging_complete: VacuumActivity.DOCKED, # "Charging complete"
RoborockStateCode.device_offline: VacuumActivity.ERROR, # "Device offline"
}
@ -112,7 +107,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
self._attr_fan_speed_list = self._device_status.fan_power_options
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Return the status of the vacuum cleaner."""
assert self._device_status.state is not None
return STATE_CODE_TO_STATE.get(self._device_status.state)

View File

@ -6,7 +6,11 @@ https://home-assistant.io/components/vacuum.romy/.
from typing import Any
from homeassistant.components.vacuum import StateVacuumEntity, VacuumEntityFeature
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -75,7 +79,14 @@ class RomyVacuumEntity(RomyEntity, StateVacuumEntity):
"""Handle updated data from the coordinator."""
self._attr_fan_speed = FAN_SPEEDS[self.romy.fan_speed]
self._attr_battery_level = self.romy.battery_level
self._attr_state = self.romy.status
if (status := self.romy.status) is None:
self._attr_activity = None
self.async_write_ha_state()
return
try:
self._attr_activity = VacuumActivity(status)
except ValueError:
self._attr_activity = None
self.async_write_ha_state()

View File

@ -8,15 +8,11 @@ from typing import Any
from homeassistant.components.vacuum import (
ATTR_STATUS,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE, STATE_PAUSED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
@ -39,16 +35,16 @@ SUPPORT_IROBOT = (
)
STATE_MAP = {
"": STATE_IDLE,
"charge": STATE_DOCKED,
"evac": STATE_RETURNING, # Emptying at cleanbase
"hmMidMsn": STATE_CLEANING, # Recharging at the middle of a cycle
"hmPostMsn": STATE_RETURNING, # Cycle finished
"hmUsrDock": STATE_RETURNING,
"pause": STATE_PAUSED,
"run": STATE_CLEANING,
"stop": STATE_IDLE,
"stuck": STATE_ERROR,
"": VacuumActivity.IDLE,
"charge": VacuumActivity.DOCKED,
"evac": VacuumActivity.RETURNING, # Emptying at cleanbase
"hmMidMsn": VacuumActivity.CLEANING, # Recharging at the middle of a cycle
"hmPostMsn": VacuumActivity.RETURNING, # Cycle finished
"hmUsrDock": VacuumActivity.RETURNING,
"pause": VacuumActivity.PAUSED,
"run": VacuumActivity.CLEANING,
"stop": VacuumActivity.IDLE,
"stuck": VacuumActivity.ERROR,
}
_LOGGER = logging.getLogger(__name__)
@ -130,7 +126,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1
@property
def _robot_state(self):
def activity(self):
"""Return the state of the vacuum cleaner."""
clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {})
cycle = clean_mission_status.get("cycle")
@ -138,16 +134,11 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
try:
state = STATE_MAP[phase]
except KeyError:
return STATE_ERROR
if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED):
state = STATE_PAUSED
return VacuumActivity.ERROR
if cycle != "none" and state in (VacuumActivity.IDLE, VacuumActivity.DOCKED):
state = VacuumActivity.PAUSED
return state
@property
def state(self) -> str:
"""Return the state of the vacuum cleaner."""
return self._robot_state
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
@ -164,7 +155,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
# Only add cleaning time and cleaned area attrs when the vacuum is
# currently on
if self.state == STATE_CLEANING:
if self.state == VacuumActivity.CLEANING:
# Get clean mission status
(
state_attrs[ATTR_CLEANING_TIME],
@ -218,7 +209,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
async def async_start(self) -> None:
"""Start or resume the cleaning task."""
if self.state == STATE_PAUSED:
if self.state == VacuumActivity.PAUSED:
await self.hass.async_add_executor_job(self.vacuum.send_command, "resume")
else:
await self.hass.async_add_executor_job(self.vacuum.send_command, "start")
@ -233,10 +224,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
if self.state == STATE_CLEANING:
if self.state == VacuumActivity.CLEANING:
await self.async_pause()
for _ in range(10):
if self.state == STATE_PAUSED:
if self.state == VacuumActivity.PAUSED:
break
await asyncio.sleep(1)
await self.hass.async_add_executor_job(self.vacuum.send_command, "dock")

View File

@ -9,12 +9,8 @@ from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum
import voluptuous as vol
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@ -30,10 +26,10 @@ from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK
from .coordinator import SharkIqUpdateCoordinator
OPERATING_STATE_MAP = {
OperatingModes.PAUSE: STATE_PAUSED,
OperatingModes.START: STATE_CLEANING,
OperatingModes.STOP: STATE_IDLE,
OperatingModes.RETURN: STATE_RETURNING,
OperatingModes.PAUSE: VacuumActivity.PAUSED,
OperatingModes.START: VacuumActivity.CLEANING,
OperatingModes.STOP: VacuumActivity.IDLE,
OperatingModes.RETURN: VacuumActivity.RETURNING,
}
FAN_SPEEDS_MAP = {
@ -156,7 +152,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME)
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Get the current vacuum state.
NB: Currently, we do not return an error state because they can be very, very stale.
@ -164,7 +160,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
user a notification.
"""
if self.sharkiq.get_property_value(Properties.CHARGING_STATUS):
return STATE_DOCKED
return VacuumActivity.DOCKED
op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE)
return OPERATING_STATE_MAP.get(op_mode)

View File

@ -5,13 +5,8 @@ from typing import Any
from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@ -43,17 +38,17 @@ async def async_setup_entry(
)
VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, str] = {
"StandBy": STATE_IDLE,
"Clearing": STATE_CLEANING,
"Paused": STATE_PAUSED,
"GotoChargeBase": STATE_RETURNING,
"Charging": STATE_DOCKED,
"ChargeDone": STATE_DOCKED,
"Dormant": STATE_IDLE,
"InTrouble": STATE_ERROR,
"InRemoteControl": STATE_CLEANING,
"InDustCollecting": STATE_DOCKED,
VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumActivity] = {
"StandBy": VacuumActivity.IDLE,
"Clearing": VacuumActivity.CLEANING,
"Paused": VacuumActivity.PAUSED,
"GotoChargeBase": VacuumActivity.RETURNING,
"Charging": VacuumActivity.DOCKED,
"ChargeDone": VacuumActivity.DOCKED,
"Dormant": VacuumActivity.IDLE,
"InTrouble": VacuumActivity.ERROR,
"InRemoteControl": VacuumActivity.CLEANING,
"InDustCollecting": VacuumActivity.DOCKED,
}
VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = {
@ -114,7 +109,7 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity):
self._attr_available = self.coordinator.data.get("onlineStatus") == "online"
switchbot_state = str(self.coordinator.data.get("workingStatus"))
self._attr_state = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state)
self._attr_activity = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state)
self.async_write_ha_state()

View File

@ -17,13 +17,8 @@ from homeassistant.components.vacuum import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import (
@ -58,12 +53,12 @@ CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}"
_VALID_STATES = [
STATE_CLEANING,
STATE_DOCKED,
STATE_PAUSED,
STATE_IDLE,
STATE_RETURNING,
STATE_ERROR,
VacuumActivity.CLEANING,
VacuumActivity.DOCKED,
VacuumActivity.PAUSED,
VacuumActivity.IDLE,
VacuumActivity.RETURNING,
VacuumActivity.ERROR,
]
VACUUM_SCHEMA = vol.All(
@ -202,7 +197,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Return the status of the vacuum cleaner."""
return self._state

View File

@ -7,13 +7,10 @@ from typing import Any
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import STATE_IDLE, STATE_PAUSED
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -24,29 +21,29 @@ from .entity import EnumTypeData, IntegerTypeData, TuyaEntity
TUYA_MODE_RETURN_HOME = "chargego"
TUYA_STATUS_TO_HA = {
"charge_done": STATE_DOCKED,
"chargecompleted": STATE_DOCKED,
"chargego": STATE_DOCKED,
"charging": STATE_DOCKED,
"cleaning": STATE_CLEANING,
"docking": STATE_RETURNING,
"goto_charge": STATE_RETURNING,
"goto_pos": STATE_CLEANING,
"mop_clean": STATE_CLEANING,
"part_clean": STATE_CLEANING,
"paused": STATE_PAUSED,
"pick_zone_clean": STATE_CLEANING,
"pos_arrived": STATE_CLEANING,
"pos_unarrive": STATE_CLEANING,
"random": STATE_CLEANING,
"sleep": STATE_IDLE,
"smart_clean": STATE_CLEANING,
"smart": STATE_CLEANING,
"spot_clean": STATE_CLEANING,
"standby": STATE_IDLE,
"wall_clean": STATE_CLEANING,
"wall_follow": STATE_CLEANING,
"zone_clean": STATE_CLEANING,
"charge_done": VacuumActivity.DOCKED,
"chargecompleted": VacuumActivity.DOCKED,
"chargego": VacuumActivity.DOCKED,
"charging": VacuumActivity.DOCKED,
"cleaning": VacuumActivity.CLEANING,
"docking": VacuumActivity.RETURNING,
"goto_charge": VacuumActivity.RETURNING,
"goto_pos": VacuumActivity.CLEANING,
"mop_clean": VacuumActivity.CLEANING,
"part_clean": VacuumActivity.CLEANING,
"paused": VacuumActivity.PAUSED,
"pick_zone_clean": VacuumActivity.CLEANING,
"pos_arrived": VacuumActivity.CLEANING,
"pos_unarrive": VacuumActivity.CLEANING,
"random": VacuumActivity.CLEANING,
"sleep": VacuumActivity.IDLE,
"smart_clean": VacuumActivity.CLEANING,
"smart": VacuumActivity.CLEANING,
"spot_clean": VacuumActivity.CLEANING,
"standby": VacuumActivity.IDLE,
"wall_clean": VacuumActivity.CLEANING,
"wall_follow": VacuumActivity.CLEANING,
"zone_clean": VacuumActivity.CLEANING,
}
@ -137,12 +134,12 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
return self.device.status.get(DPCode.SUCTION)
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Return Tuya vacuum device state."""
if self.device.status.get(DPCode.PAUSE) and not (
self.device.status.get(DPCode.STATUS)
):
return STATE_PAUSED
return VacuumActivity.PAUSED
if not (status := self.device.status.get(DPCode.STATUS)):
return None
return TUYA_STATUS_TO_HA.get(status)

View File

@ -2,11 +2,12 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
from enum import IntFlag
from functools import partial
import logging
from typing import Any
from typing import TYPE_CHECKING, Any, final
from propcache import cached_property
import voluptuous as vol
@ -18,11 +19,9 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_IDLE,
STATE_ON,
STATE_PAUSED,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
@ -32,12 +31,21 @@ from homeassistant.helpers.deprecation import (
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.frame import ReportBehavior, report_usage
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
from .const import ( # noqa: F401
_DEPRECATED_STATE_CLEANING,
_DEPRECATED_STATE_DOCKED,
_DEPRECATED_STATE_ERROR,
_DEPRECATED_STATE_RETURNING,
DOMAIN,
VacuumActivity,
)
_LOGGER = logging.getLogger(__name__)
@ -64,11 +72,13 @@ SERVICE_START = "start"
SERVICE_PAUSE = "pause"
SERVICE_STOP = "stop"
STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR]
DEFAULT_NAME = "Vacuum cleaner robot"
# These STATE_* constants are deprecated as of Home Assistant 2025.1.
# Please use the VacuumActivity enum instead.
_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
class VacuumEntityFeature(IntFlag):
"""Supported features of the vacuum entity."""
@ -216,7 +226,7 @@ STATE_VACUUM_CACHED_PROPERTIES_WITH_ATTR_ = {
"battery_icon",
"fan_speed",
"fan_speed_list",
"state",
"activity",
}
@ -233,9 +243,58 @@ class StateVacuumEntity(
_attr_battery_level: int | None = None
_attr_fan_speed: str | None = None
_attr_fan_speed_list: list[str]
_attr_state: str | None = None
_attr_activity: VacuumActivity | None = None
_attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
__vacuum_legacy_state: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
# Integrations should use the 'activity' property instead of
# setting the state directly.
cls.__vacuum_legacy_state = True
def __setattr__(self, name: str, value: Any) -> None:
"""Set attribute.
Deprecation warning if setting '_attr_state' directly
unless already reported.
"""
if name == "_attr_state":
self._report_deprecated_activity_handling()
return super().__setattr__(name, value)
@callback
def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)
if self.__vacuum_legacy_state:
self._report_deprecated_activity_handling()
@callback
def _report_deprecated_activity_handling(self) -> None:
"""Report on deprecated handling of vacuum state.
Integrations should implement activity instead of using state directly.
"""
report_usage(
"is setting state directly."
f" Entity {self.entity_id} ({type(self)}) should implement the 'activity'"
" property and return its state using the VacuumActivity enum",
core_integration_behavior=ReportBehavior.ERROR,
custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.1",
integration_domain=self.platform.platform_name if self.platform else None,
exclude_integrations={DOMAIN},
)
@cached_property
def battery_level(self) -> int | None:
"""Return the battery level of the vacuum cleaner."""
@ -244,7 +303,7 @@ class StateVacuumEntity(
@property
def battery_icon(self) -> str:
"""Return the battery icon for the vacuum cleaner."""
charging = bool(self.state == STATE_DOCKED)
charging = bool(self.activity == VacuumActivity.DOCKED)
return icon_for_battery_level(
battery_level=self.battery_level, charging=charging
@ -282,10 +341,28 @@ class StateVacuumEntity(
return data
@cached_property
@final
@property
def state(self) -> str | None:
"""Return the state of the vacuum cleaner."""
return self._attr_state
if (activity := self.activity) is not None:
return activity
if self._attr_state is not None:
# Backwards compatibility for integrations that set state directly
# Should be removed in 2026.1
if TYPE_CHECKING:
assert isinstance(self._attr_state, str)
return self._attr_state
return None
@cached_property
def activity(self) -> VacuumActivity | None:
"""Return the current vacuum activity.
Integrations should overwrite this or use the '_attr_activity'
attribute to set the vacuum activity using the 'VacuumActivity' enum.
"""
return self._attr_activity
@cached_property
def supported_features(self) -> VacuumEntityFeature:

View File

@ -1,10 +1,42 @@
"""Support for vacuum cleaner robots (botvacs)."""
from __future__ import annotations
from enum import StrEnum
from functools import partial
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
DOMAIN = "vacuum"
STATE_CLEANING = "cleaning"
STATE_DOCKED = "docked"
STATE_RETURNING = "returning"
STATE_ERROR = "error"
STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR]
class VacuumActivity(StrEnum):
"""Vacuum activity states."""
CLEANING = "cleaning"
DOCKED = "docked"
IDLE = "idle"
PAUSED = "paused"
RETURNING = "returning"
ERROR = "error"
# These STATE_* constants are deprecated as of Home Assistant 2025.1.
# Please use the VacuumActivity enum instead.
_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumActivity.CLEANING, "2026.1")
_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumActivity.DOCKED, "2026.1")
_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum(VacuumActivity.RETURNING, "2026.1")
_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumActivity.ERROR, "2026.1")
# 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())

View File

@ -20,7 +20,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING
from . import DOMAIN, VacuumActivity
CONDITION_TYPES = {"is_cleaning", "is_docked"}
@ -62,9 +62,9 @@ def async_condition_from_config(
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_docked":
test_states = [STATE_DOCKED]
test_states = [VacuumActivity.DOCKED]
else:
test_states = [STATE_CLEANING, STATE_RETURNING]
test_states = [VacuumActivity.CLEANING, VacuumActivity.RETURNING]
registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID])

View File

@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN, STATE_CLEANING, STATE_DOCKED
from . import DOMAIN, VacuumActivity
TRIGGER_TYPES = {"cleaning", "docked"}
@ -77,9 +77,9 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach a trigger."""
if config[CONF_TYPE] == "cleaning":
to_state = STATE_CLEANING
to_state = VacuumActivity.CLEANING
else:
to_state = STATE_DOCKED
to_state = VacuumActivity.DOCKED
state_config = {
CONF_PLATFORM: "state",

View File

@ -11,10 +11,8 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_IDLE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
)
from homeassistant.core import Context, HomeAssistant, State
@ -26,20 +24,18 @@ from . import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_DOCKED,
STATE_RETURNING,
VacuumActivity,
)
_LOGGER = logging.getLogger(__name__)
VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF}
VALID_STATES_STATE = {
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
VacuumActivity.CLEANING,
VacuumActivity.DOCKED,
VacuumActivity.IDLE,
VacuumActivity.PAUSED,
VacuumActivity.RETURNING,
}
@ -75,13 +71,13 @@ async def _async_reproduce_state(
service = SERVICE_TURN_ON
elif state.state == STATE_OFF:
service = SERVICE_TURN_OFF
elif state.state == STATE_CLEANING:
elif state.state == VacuumActivity.CLEANING:
service = SERVICE_START
elif state.state in [STATE_DOCKED, STATE_RETURNING]:
elif state.state in [VacuumActivity.DOCKED, VacuumActivity.RETURNING]:
service = SERVICE_RETURN_TO_BASE
elif state.state == STATE_IDLE:
elif state.state == VacuumActivity.IDLE:
service = SERVICE_STOP
elif state.state == STATE_PAUSED:
elif state.state == VacuumActivity.PAUSED:
service = SERVICE_PAUSE
await hass.services.async_call(

View File

@ -10,13 +10,8 @@ from miio import DeviceException
import voluptuous as vol
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@ -55,29 +50,29 @@ ATTR_ZONE_REPEATER = "repeats"
ATTR_TIMERS = "timers"
STATE_CODE_TO_STATE = {
1: STATE_IDLE, # "Starting"
2: STATE_IDLE, # "Charger disconnected"
3: STATE_IDLE, # "Idle"
4: STATE_CLEANING, # "Remote control active"
5: STATE_CLEANING, # "Cleaning"
6: STATE_RETURNING, # "Returning home"
7: STATE_CLEANING, # "Manual mode"
8: STATE_DOCKED, # "Charging"
9: STATE_ERROR, # "Charging problem"
10: STATE_PAUSED, # "Paused"
11: STATE_CLEANING, # "Spot cleaning"
12: STATE_ERROR, # "Error"
13: STATE_IDLE, # "Shutting down"
14: STATE_DOCKED, # "Updating"
15: STATE_RETURNING, # "Docking"
16: STATE_CLEANING, # "Going to target"
17: STATE_CLEANING, # "Zoned cleaning"
18: STATE_CLEANING, # "Segment cleaning"
22: STATE_DOCKED, # "Emptying the bin" on s7+
23: STATE_DOCKED, # "Washing the mop" on s7maxV
26: STATE_RETURNING, # "Going to wash the mop" on s7maxV
100: STATE_DOCKED, # "Charging complete"
101: STATE_ERROR, # "Device offline"
1: VacuumActivity.IDLE, # "Starting"
2: VacuumActivity.IDLE, # "Charger disconnected"
3: VacuumActivity.IDLE, # "Idle"
4: VacuumActivity.CLEANING, # "Remote control active"
5: VacuumActivity.CLEANING, # "Cleaning"
6: VacuumActivity.RETURNING, # "Returning home"
7: VacuumActivity.CLEANING, # "Manual mode"
8: VacuumActivity.DOCKED, # "Charging"
9: VacuumActivity.ERROR, # "Charging problem"
10: VacuumActivity.PAUSED, # "Paused"
11: VacuumActivity.CLEANING, # "Spot cleaning"
12: VacuumActivity.ERROR, # "Error"
13: VacuumActivity.IDLE, # "Shutting down"
14: VacuumActivity.DOCKED, # "Updating"
15: VacuumActivity.RETURNING, # "Docking"
16: VacuumActivity.CLEANING, # "Going to target"
17: VacuumActivity.CLEANING, # "Zoned cleaning"
18: VacuumActivity.CLEANING, # "Segment cleaning"
22: VacuumActivity.DOCKED, # "Emptying the bin" on s7+
23: VacuumActivity.DOCKED, # "Washing the mop" on s7maxV
26: VacuumActivity.RETURNING, # "Going to wash the mop" on s7maxV
100: VacuumActivity.DOCKED, # "Charging complete"
101: VacuumActivity.ERROR, # "Device offline"
}
@ -211,7 +206,7 @@ class MiroboVacuum(
) -> None:
"""Initialize the Xiaomi vacuum cleaner robot handler."""
super().__init__(device, entry, unique_id, coordinator)
self._state: str | None = None
self._state: VacuumActivity | None = None
async def async_added_to_hass(self) -> None:
"""Run when entity is about to be added to hass."""
@ -219,12 +214,12 @@ class MiroboVacuum(
self._handle_coordinator_update()
@property
def state(self) -> str | None:
def activity(self) -> VacuumActivity | None:
"""Return the status of the vacuum cleaner."""
# The vacuum reverts back to an idle state after erroring out.
# We want to keep returning an error until it has been cleared.
if self.coordinator.data.status.got_error:
return STATE_ERROR
return VacuumActivity.ERROR
return self._state

View File

@ -22,11 +22,7 @@ from homeassistant.components.vacuum import (
DOMAIN as VACUUM_DOMAIN,
SERVICE_SEND_COMMAND,
SERVICE_SET_FAN_SPEED,
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
VacuumActivity,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -75,35 +71,35 @@ async def test_supported_features(hass: HomeAssistant) -> None:
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 100
assert state.attributes.get(ATTR_FAN_SPEED) == "medium"
assert state.attributes.get(ATTR_FAN_SPEED_LIST) == FAN_SPEEDS
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
state = hass.states.get(ENTITY_VACUUM_MOST)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 12412
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 100
assert state.attributes.get(ATTR_FAN_SPEED) == "medium"
assert state.attributes.get(ATTR_FAN_SPEED_LIST) == FAN_SPEEDS
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
state = hass.states.get(ENTITY_VACUUM_BASIC)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 12360
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 100
assert state.attributes.get(ATTR_FAN_SPEED) is None
assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
state = hass.states.get(ENTITY_VACUUM_MINIMAL)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 3
assert state.attributes.get(ATTR_BATTERY_LEVEL) is None
assert state.attributes.get(ATTR_FAN_SPEED) is None
assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
state = hass.states.get(ENTITY_VACUUM_NONE)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0
assert state.attributes.get(ATTR_BATTERY_LEVEL) is None
assert state.attributes.get(ATTR_FAN_SPEED) is None
assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
async def test_methods(hass: HomeAssistant) -> None:
@ -111,29 +107,29 @@ async def test_methods(hass: HomeAssistant) -> None:
await common.async_start(hass, ENTITY_VACUUM_BASIC)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_BASIC)
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
await common.async_stop(hass, ENTITY_VACUUM_BASIC)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_BASIC)
assert state.state == STATE_IDLE
assert state.state == VacuumActivity.IDLE
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
await hass.async_block_till_done()
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 100
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
await async_setup_component(hass, "notify", {})
await hass.async_block_till_done()
await common.async_locate(hass, ENTITY_VACUUM_COMPLETE)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_IDLE
assert state.state == VacuumActivity.IDLE
await common.async_return_to_base(hass, ENTITY_VACUUM_COMPLETE)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_RETURNING
assert state.state == VacuumActivity.RETURNING
await common.async_set_fan_speed(
hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_COMPLETE
@ -145,21 +141,21 @@ async def test_methods(hass: HomeAssistant) -> None:
await common.async_clean_spot(hass, ENTITY_VACUUM_COMPLETE)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
await common.async_pause(hass, ENTITY_VACUUM_COMPLETE)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_PAUSED
assert state.state == VacuumActivity.PAUSED
await common.async_return_to_base(hass, ENTITY_VACUUM_COMPLETE)
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_RETURNING
assert state.state == VacuumActivity.RETURNING
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
state = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
async def test_unsupported_methods(hass: HomeAssistant) -> None:
@ -251,4 +247,4 @@ async def test_send_command(hass: HomeAssistant) -> None:
new_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert old_state_complete != new_state_complete
assert new_state_complete.state == STATE_IDLE
assert new_state_complete.state == VacuumActivity.IDLE

View File

@ -431,7 +431,9 @@ async def test_dock_vacuum(hass: HomeAssistant) -> None:
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None, None)
trt = trait.DockTrait(hass, State("vacuum.bla", vacuum.STATE_IDLE), BASIC_CONFIG)
trt = trait.DockTrait(
hass, State("vacuum.bla", vacuum.VacuumActivity.IDLE), BASIC_CONFIG
)
assert trt.sync_attributes() == {}
@ -454,7 +456,7 @@ async def test_locate_vacuum(hass: HomeAssistant) -> None:
hass,
State(
"vacuum.bla",
vacuum.STATE_IDLE,
vacuum.VacuumActivity.IDLE,
{ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.LOCATE},
),
BASIC_CONFIG,
@ -485,7 +487,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None:
hass,
State(
"vacuum.bla",
vacuum.STATE_DOCKED,
vacuum.VacuumActivity.DOCKED,
{
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY,
ATTR_BATTERY_LEVEL: 100,
@ -511,7 +513,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None:
hass,
State(
"vacuum.bla",
vacuum.STATE_CLEANING,
vacuum.VacuumActivity.CLEANING,
{
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY,
ATTR_BATTERY_LEVEL: 20,
@ -551,7 +553,7 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None:
hass,
State(
"vacuum.bla",
vacuum.STATE_PAUSED,
vacuum.VacuumActivity.PAUSED,
{ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE},
),
BASIC_CONFIG,

View File

@ -26,8 +26,7 @@ from homeassistant.components.vacuum import (
SERVICE_START,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_CLEANING,
STATE_DOCKED,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import (
@ -295,7 +294,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support(
hass.states.async_set(
entity_id,
STATE_CLEANING,
VacuumActivity.CLEANING,
{
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.START
@ -306,7 +305,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support(
hass.states.async_set(
entity_id,
STATE_DOCKED,
VacuumActivity.DOCKED,
{
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.START

View File

@ -9,7 +9,7 @@ from homeassistant.components import litterrobot
from homeassistant.components.vacuum import (
DOMAIN as VACUUM_DOMAIN,
SERVICE_START,
STATE_DOCKED,
VacuumActivity,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID
@ -30,7 +30,7 @@ async def test_unload_entry(hass: HomeAssistant, mock_account: MagicMock) -> Non
vacuum = hass.states.get(VACUUM_ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_DOCKED
assert vacuum.state == VacuumActivity.DOCKED
await hass.services.async_call(
VACUUM_DOMAIN,

View File

@ -15,9 +15,7 @@ from homeassistant.components.vacuum import (
DOMAIN as PLATFORM_DOMAIN,
SERVICE_START,
SERVICE_STOP,
STATE_DOCKED,
STATE_ERROR,
STATE_PAUSED,
VacuumActivity,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
@ -53,7 +51,7 @@ async def test_vacuum(
vacuum = hass.states.get(VACUUM_ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_DOCKED
assert vacuum.state == VacuumActivity.DOCKED
assert vacuum.attributes["is_sleeping"] is False
ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID)
@ -95,18 +93,21 @@ async def test_vacuum_with_error(
vacuum = hass.states.get(VACUUM_ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_ERROR
assert vacuum.state == VacuumActivity.ERROR
@pytest.mark.parametrize(
("robot_data", "expected_state"),
[
({"displayCode": "DC_CAT_DETECT"}, STATE_DOCKED),
({"isDFIFull": True}, STATE_ERROR),
({"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, STATE_PAUSED),
({"displayCode": "DC_CAT_DETECT"}, VacuumActivity.DOCKED),
({"isDFIFull": True}, VacuumActivity.ERROR),
(
{"robotCycleState": "CYCLE_STATE_CAT_DETECT"},
VacuumActivity.PAUSED,
),
],
)
async def test_vacuum_states(
async def test_activities(
hass: HomeAssistant,
mock_account_with_litterrobot_4: MagicMock,
robot_data: dict[str, str | bool],
@ -150,7 +151,7 @@ async def test_commands(
vacuum = hass.states.get(VACUUM_ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_DOCKED
assert vacuum.state == VacuumActivity.DOCKED
extra = extra or {}
data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})}

View File

@ -27,8 +27,7 @@ from homeassistant.components.vacuum import (
SERVICE_RETURN_TO_BASE,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_DOCKED,
VacuumActivity,
)
from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
@ -313,7 +312,7 @@ async def test_status(
}"""
async_fire_mqtt_message(hass, "vacuum/state", message)
state = hass.states.get("vacuum.mqtttest")
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50"
assert state.attributes.get(ATTR_FAN_SPEED) == "max"
@ -326,7 +325,7 @@ async def test_status(
async_fire_mqtt_message(hass, "vacuum/state", message)
state = hass.states.get("vacuum.mqtttest")
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60"
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61
assert state.attributes.get(ATTR_FAN_SPEED) == "min"
@ -366,7 +365,7 @@ async def test_no_fan_vacuum(
}"""
async_fire_mqtt_message(hass, "vacuum/state", message)
state = hass.states.get("vacuum.mqtttest")
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
assert state.attributes.get(ATTR_FAN_SPEED) is None
assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54
@ -380,7 +379,7 @@ async def test_no_fan_vacuum(
async_fire_mqtt_message(hass, "vacuum/state", message)
state = hass.states.get("vacuum.mqtttest")
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
assert state.attributes.get(ATTR_FAN_SPEED) is None
assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None
@ -394,7 +393,7 @@ async def test_no_fan_vacuum(
async_fire_mqtt_message(hass, "vacuum/state", message)
state = hass.states.get("vacuum.mqtttest")
assert state.state == STATE_DOCKED
assert state.state == VacuumActivity.DOCKED
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60"
assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61

View File

@ -35,10 +35,7 @@ from homeassistant.components.vacuum import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import (
@ -160,7 +157,7 @@ async def test_simple_properties(
assert entity
assert state
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
assert entity.unique_id == "AC000Wxxxxxxxxx"
@ -189,10 +186,10 @@ async def test_initial_attributes(
@pytest.mark.parametrize(
("service", "target_state"),
[
(SERVICE_STOP, STATE_IDLE),
(SERVICE_PAUSE, STATE_PAUSED),
(SERVICE_RETURN_TO_BASE, STATE_RETURNING),
(SERVICE_START, STATE_CLEANING),
(SERVICE_STOP, VacuumActivity.IDLE),
(SERVICE_PAUSE, VacuumActivity.PAUSED),
(SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING),
(SERVICE_START, VacuumActivity.CLEANING),
],
)
async def test_cleaning_states(

View File

@ -3,14 +3,7 @@
import pytest
from homeassistant import setup
from homeassistant.components.vacuum import (
ATTR_BATTERY_LEVEL,
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
)
from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumActivity
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
@ -44,7 +37,7 @@ _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level"
},
),
(
STATE_CLEANING,
VacuumActivity.CLEANING,
100,
{
"vacuum": {
@ -149,10 +142,10 @@ async def test_templates_with_entities(hass: HomeAssistant) -> None:
"""Test templates with values from other entities."""
_verify(hass, STATE_UNKNOWN, None)
hass.states.async_set(_STATE_INPUT_SELECT, STATE_CLEANING)
hass.states.async_set(_STATE_INPUT_SELECT, VacuumActivity.CLEANING)
hass.states.async_set(_BATTERY_LEVEL_INPUT_NUMBER, 100)
await hass.async_block_till_done()
_verify(hass, STATE_CLEANING, 100)
_verify(hass, VacuumActivity.CLEANING, 100)
@pytest.mark.parametrize(
@ -370,8 +363,8 @@ async def test_state_services(hass: HomeAssistant, calls: list[ServiceCall]) ->
await hass.async_block_till_done()
# verify
assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_CLEANING
_verify(hass, STATE_CLEANING, None)
assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.CLEANING
_verify(hass, VacuumActivity.CLEANING, None)
assert len(calls) == 1
assert calls[-1].data["action"] == "start"
assert calls[-1].data["caller"] == _TEST_VACUUM
@ -381,8 +374,8 @@ async def test_state_services(hass: HomeAssistant, calls: list[ServiceCall]) ->
await hass.async_block_till_done()
# verify
assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_PAUSED
_verify(hass, STATE_PAUSED, None)
assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.PAUSED
_verify(hass, VacuumActivity.PAUSED, None)
assert len(calls) == 2
assert calls[-1].data["action"] == "pause"
assert calls[-1].data["caller"] == _TEST_VACUUM
@ -392,8 +385,8 @@ async def test_state_services(hass: HomeAssistant, calls: list[ServiceCall]) ->
await hass.async_block_till_done()
# verify
assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_IDLE
_verify(hass, STATE_IDLE, None)
assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.IDLE
_verify(hass, VacuumActivity.IDLE, None)
assert len(calls) == 3
assert calls[-1].data["action"] == "stop"
assert calls[-1].data["caller"] == _TEST_VACUUM
@ -403,8 +396,8 @@ async def test_state_services(hass: HomeAssistant, calls: list[ServiceCall]) ->
await hass.async_block_till_done()
# verify
assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_RETURNING
_verify(hass, STATE_RETURNING, None)
assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.RETURNING
_verify(hass, VacuumActivity.RETURNING, None)
assert len(calls) == 4
assert calls[-1].data["action"] == "return_to_base"
assert calls[-1].data["caller"] == _TEST_VACUUM
@ -506,7 +499,11 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None:
assert await setup.async_setup_component(
hass,
"input_select",
{"input_select": {"state": {"name": "State", "options": [STATE_CLEANING]}}},
{
"input_select": {
"state": {"name": "State", "options": [VacuumActivity.CLEANING]}
}
},
)
with assert_setup_component(1, "vacuum"):
@ -522,7 +519,7 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None:
"service": "input_select.select_option",
"data": {
"entity_id": _STATE_INPUT_SELECT,
"option": STATE_CLEANING,
"option": VacuumActivity.CLEANING,
},
}
}
@ -554,11 +551,11 @@ async def _register_components(hass: HomeAssistant) -> None:
"state": {
"name": "State",
"options": [
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
VacuumActivity.CLEANING,
VacuumActivity.DOCKED,
VacuumActivity.IDLE,
VacuumActivity.PAUSED,
VacuumActivity.RETURNING,
],
},
"fan_speed": {
@ -578,7 +575,7 @@ async def _register_components(hass: HomeAssistant) -> None:
"service": "input_select.select_option",
"data": {
"entity_id": _STATE_INPUT_SELECT,
"option": STATE_CLEANING,
"option": VacuumActivity.CLEANING,
},
},
{
@ -592,7 +589,10 @@ async def _register_components(hass: HomeAssistant) -> None:
"pause": [
{
"service": "input_select.select_option",
"data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_PAUSED},
"data": {
"entity_id": _STATE_INPUT_SELECT,
"option": VacuumActivity.PAUSED,
},
},
{
"service": "test.automation",
@ -605,7 +605,10 @@ async def _register_components(hass: HomeAssistant) -> None:
"stop": [
{
"service": "input_select.select_option",
"data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_IDLE},
"data": {
"entity_id": _STATE_INPUT_SELECT,
"option": VacuumActivity.IDLE,
},
},
{
"service": "test.automation",
@ -620,7 +623,7 @@ async def _register_components(hass: HomeAssistant) -> None:
"service": "input_select.select_option",
"data": {
"entity_id": _STATE_INPUT_SELECT,
"option": STATE_RETURNING,
"option": VacuumActivity.RETURNING,
},
},
{

View File

@ -4,12 +4,8 @@ from typing import Any
from homeassistant.components.vacuum import (
DOMAIN,
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
@ -39,20 +35,20 @@ class MockVacuum(MockEntity, StateVacuumEntity):
def __init__(self, **values: Any) -> None:
"""Initialize a mock vacuum entity."""
super().__init__(**values)
self._attr_state = STATE_DOCKED
self._attr_activity = VacuumActivity.DOCKED
self._attr_fan_speed = "slow"
def stop(self, **kwargs: Any) -> None:
"""Stop cleaning."""
self._attr_state = STATE_IDLE
self._attr_activity = VacuumActivity.IDLE
def return_to_base(self, **kwargs: Any) -> None:
"""Return to base."""
self._attr_state = STATE_RETURNING
self._attr_activity = VacuumActivity.RETURNING
def clean_spot(self, **kwargs: Any) -> None:
"""Clean a spot."""
self._attr_state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set the fan speed."""
@ -60,11 +56,11 @@ class MockVacuum(MockEntity, StateVacuumEntity):
def start(self) -> None:
"""Start cleaning."""
self._attr_state = STATE_CLEANING
self._attr_activity = VacuumActivity.CLEANING
def pause(self) -> None:
"""Pause cleaning."""
self._attr_state = STATE_PAUSED
self._attr_activity = VacuumActivity.PAUSED
async def help_async_setup_entry_init(

View File

@ -1,13 +1,28 @@
"""Fixtures for Vacuum platform tests."""
from collections.abc import Generator
from collections.abc import AsyncGenerator, Generator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.config_entries import ConfigFlow
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN, VacuumEntityFeature
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, frame
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from tests.common import mock_config_flow, mock_platform
from . import MockVacuum
from tests.common import (
MockConfigEntry,
MockModule,
MockPlatform,
mock_config_flow,
mock_integration,
mock_platform,
)
TEST_DOMAIN = "test"
class MockFlow(ConfigFlow):
@ -17,7 +32,94 @@ class MockFlow(ConfigFlow):
@pytest.fixture
def config_flow_fixture(hass: HomeAssistant) -> Generator[None]:
"""Mock config flow."""
mock_platform(hass, "test.config_flow")
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
with mock_config_flow("test", MockFlow):
with mock_config_flow(TEST_DOMAIN, MockFlow):
yield
@pytest.fixture(name="supported_features")
async def vacuum_supported_features() -> VacuumEntityFeature:
"""Return the supported features for the test vacuum entity."""
return (
VacuumEntityFeature.PAUSE
| VacuumEntityFeature.STOP
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.MAP
| VacuumEntityFeature.STATE
| VacuumEntityFeature.START
)
@pytest.fixture(name="mock_vacuum_entity")
async def setup_vacuum_platform_test_entity(
hass: HomeAssistant,
config_flow_fixture: None,
entity_registry: er.EntityRegistry,
supported_features: VacuumEntityFeature,
) -> MagicMock:
"""Set up vacuum entity using an entity platform."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [VACUUM_DOMAIN]
)
return True
mock_integration(
hass,
MockModule(
TEST_DOMAIN,
async_setup_entry=async_setup_entry_init,
),
)
entity = MockVacuum(
supported_features=supported_features,
)
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up test vacuum platform via config entry."""
async_add_entities([entity])
mock_platform(
hass,
f"{TEST_DOMAIN}.{VACUUM_DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity.entity_id)
assert state is not None
return entity
@pytest.fixture(name="mock_as_custom_component")
async def mock_frame(hass: HomeAssistant) -> AsyncGenerator[None]:
"""Mock frame."""
with patch(
"homeassistant.helpers.frame.get_integration_frame",
return_value=frame.IntegrationFrame(
custom_integration=True,
integration="alarm_control_panel",
module="test_init.py",
relative_filename="test_init.py",
frame=frame.get_current_frame(),
),
):
yield

View File

@ -5,12 +5,7 @@ from pytest_unordered import unordered
from homeassistant.components import automation
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.vacuum import (
DOMAIN,
STATE_CLEANING,
STATE_DOCKED,
STATE_RETURNING,
)
from homeassistant.components.vacuum import DOMAIN, VacuumActivity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -122,7 +117,7 @@ async def test_if_state(
DOMAIN, "test", "5678", device_id=device_entry.id
)
hass.states.async_set(entry.entity_id, STATE_DOCKED)
hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED)
assert await async_setup_component(
hass,
@ -174,7 +169,7 @@ async def test_if_state(
assert len(service_calls) == 1
assert service_calls[0].data["some"] == "is_docked - event - test_event2"
hass.states.async_set(entry.entity_id, STATE_CLEANING)
hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
@ -182,7 +177,7 @@ async def test_if_state(
assert service_calls[1].data["some"] == "is_cleaning - event - test_event1"
# Returning means it's still cleaning
hass.states.async_set(entry.entity_id, STATE_RETURNING)
hass.states.async_set(entry.entity_id, VacuumActivity.RETURNING)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
@ -207,7 +202,7 @@ async def test_if_state_legacy(
DOMAIN, "test", "5678", device_id=device_entry.id
)
hass.states.async_set(entry.entity_id, STATE_CLEANING)
hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING)
assert await async_setup_component(
hass,

View File

@ -7,7 +7,7 @@ from pytest_unordered import unordered
from homeassistant.components import automation
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED
from homeassistant.components.vacuum import DOMAIN, VacuumActivity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -188,7 +188,7 @@ async def test_if_fires_on_state_change(
DOMAIN, "test", "5678", device_id=device_entry.id
)
hass.states.async_set(entry.entity_id, STATE_DOCKED)
hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED)
assert await async_setup_component(
hass,
@ -238,7 +238,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is cleaning
hass.states.async_set(entry.entity_id, STATE_CLEANING)
hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert (
@ -247,7 +247,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is docked
hass.states.async_set(entry.entity_id, STATE_DOCKED)
hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED)
await hass.async_block_till_done()
assert len(service_calls) == 2
assert (
@ -273,7 +273,7 @@ async def test_if_fires_on_state_change_legacy(
DOMAIN, "test", "5678", device_id=device_entry.id
)
hass.states.async_set(entry.entity_id, STATE_DOCKED)
hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED)
assert await async_setup_component(
hass,
@ -304,7 +304,7 @@ async def test_if_fires_on_state_change_legacy(
)
# Fake that the entity is cleaning
hass.states.async_set(entry.entity_id, STATE_CLEANING)
hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert (
@ -330,7 +330,7 @@ async def test_if_fires_on_state_change_with_for(
DOMAIN, "test", "5678", device_id=device_entry.id
)
hass.states.async_set(entry.entity_id, STATE_DOCKED)
hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED)
assert await async_setup_component(
hass,
@ -365,7 +365,7 @@ async def test_if_fires_on_state_change_with_for(
await hass.async_block_till_done()
assert len(service_calls) == 0
hass.states.async_set(entry.entity_id, STATE_CLEANING)
hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING)
await hass.async_block_till_done()
assert len(service_calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))

View File

@ -5,12 +5,13 @@ from __future__ import annotations
from enum import Enum
from types import ModuleType
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
DOMAIN,
DOMAIN as VACUUM_DOMAIN,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
@ -19,19 +20,19 @@ from homeassistant.components.vacuum import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import frame
from . import MockVacuum, help_async_setup_entry_init, help_async_unload_entry
from .common import async_start
from tests.common import (
MockConfigEntry,
MockEntity,
MockModule,
help_test_all,
import_and_test_deprecated_constant_enum,
@ -72,14 +73,33 @@ def test_deprecated_constants(
)
@pytest.mark.parametrize(
("enum", "constant_prefix"), _create_tuples(vacuum.VacuumActivity, "STATE_")
)
@pytest.mark.parametrize(
"module",
[vacuum],
)
def test_deprecated_constants_for_state(
caplog: pytest.LogCaptureFixture,
enum: Enum,
constant_prefix: str,
module: ModuleType,
) -> None:
"""Test deprecated constants."""
import_and_test_deprecated_constant_enum(
caplog, module, enum, constant_prefix, "2026.1"
)
@pytest.mark.parametrize(
("service", "expected_state"),
[
(SERVICE_CLEAN_SPOT, STATE_CLEANING),
(SERVICE_PAUSE, STATE_PAUSED),
(SERVICE_RETURN_TO_BASE, STATE_RETURNING),
(SERVICE_START, STATE_CLEANING),
(SERVICE_STOP, STATE_IDLE),
(SERVICE_CLEAN_SPOT, VacuumActivity.CLEANING),
(SERVICE_PAUSE, VacuumActivity.PAUSED),
(SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING),
(SERVICE_START, VacuumActivity.CLEANING),
(SERVICE_STOP, VacuumActivity.IDLE),
],
)
async def test_state_services(
@ -101,18 +121,20 @@ async def test_state_services(
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True)
setup_test_component_platform(
hass, VACUUM_DOMAIN, [mock_vacuum], from_config_entry=True
)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
VACUUM_DOMAIN,
service,
{"entity_id": mock_vacuum.entity_id},
blocking=True,
)
vacuum_state = hass.states.get(mock_vacuum.entity_id)
activity = hass.states.get(mock_vacuum.entity_id)
assert vacuum_state.state == expected_state
assert activity.state == expected_state
async def test_fan_speed(hass: HomeAssistant, config_flow_fixture: None) -> None:
@ -132,14 +154,16 @@ async def test_fan_speed(hass: HomeAssistant, config_flow_fixture: None) -> None
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True)
setup_test_component_platform(
hass, VACUUM_DOMAIN, [mock_vacuum], from_config_entry=True
)
assert await hass.config_entries.async_setup(config_entry.entry_id)
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
await hass.services.async_call(
DOMAIN,
VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": mock_vacuum.entity_id, "fan_speed": "high"},
blocking=True,
@ -178,11 +202,13 @@ async def test_locate(hass: HomeAssistant, config_flow_fixture: None) -> None:
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True)
setup_test_component_platform(
hass, VACUUM_DOMAIN, [mock_vacuum], from_config_entry=True
)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
VACUUM_DOMAIN,
SERVICE_LOCATE,
{"entity_id": mock_vacuum.entity_id},
blocking=True,
@ -227,11 +253,13 @@ async def test_send_command(hass: HomeAssistant, config_flow_fixture: None) -> N
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True)
setup_test_component_platform(
hass, VACUUM_DOMAIN, [mock_vacuum], from_config_entry=True
)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
VACUUM_DOMAIN,
SERVICE_SEND_COMMAND,
{
"entity_id": mock_vacuum.entity_id,
@ -278,3 +306,178 @@ async def test_supported_features_compat(hass: HomeAssistant) -> None:
"fan_speed_list": ["silent", "normal", "pet hair"]
}
assert entity._deprecated_supported_features_reported
async def test_vacuum_not_log_deprecated_state_warning(
hass: HomeAssistant,
mock_vacuum_entity: MockVacuum,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test correctly using activity doesn't log issue or raise repair."""
state = hass.states.get(mock_vacuum_entity.entity_id)
assert state is not None
assert (
"should implement the 'activity' property and return its state using the VacuumActivity enum"
not in caplog.text
)
@pytest.mark.usefixtures("mock_as_custom_component")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_vacuum_log_deprecated_state_warning_using_state_prop(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test incorrectly using state property does log issue and raise repair."""
class MockLegacyVacuum(MockVacuum):
"""Mocked vacuum entity."""
@property
def state(self) -> str:
"""Return the state of the entity."""
return VacuumActivity.CLEANING
entity = MockLegacyVacuum(
name="Testing",
entity_id="vacuum.test",
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, VACUUM_DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
state = hass.states.get(entity.entity_id)
assert state is not None
assert (
"should implement the 'activity' property and return its state using the VacuumActivity enum"
in caplog.text
)
@pytest.mark.usefixtures("mock_as_custom_component")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test incorrectly using _attr_state attribute does log issue and raise repair."""
class MockLegacyVacuum(MockVacuum):
"""Mocked vacuum entity."""
def start(self) -> None:
"""Start cleaning."""
self._attr_state = VacuumActivity.CLEANING
entity = MockLegacyVacuum(
name="Testing",
entity_id="vacuum.test",
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, VACUUM_DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
state = hass.states.get(entity.entity_id)
assert state is not None
assert (
"should implement the 'activity' property and return its state using the VacuumActivity enum"
not in caplog.text
)
await async_start(hass, entity.entity_id)
assert (
"should implement the 'activity' property and return its state using the VacuumActivity enum"
in caplog.text
)
caplog.clear()
await async_start(hass, entity.entity_id)
# Test we only log once
assert (
"should implement the 'activity' property and return its state using the VacuumActivity enum"
not in caplog.text
)
@pytest.mark.usefixtures("mock_as_custom_component")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_alarm_control_panel_deprecated_state_does_not_break_state(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test using _attr_state attribute does not break state."""
class MockLegacyVacuum(MockEntity, StateVacuumEntity):
"""Mocked vacuum entity."""
_attr_supported_features = VacuumEntityFeature.STATE | VacuumEntityFeature.START
def __init__(self, **values: Any) -> None:
"""Initialize a mock vacuum entity."""
super().__init__(**values)
self._attr_state = VacuumActivity.DOCKED
def start(self) -> None:
"""Start cleaning."""
self._attr_state = VacuumActivity.CLEANING
entity = MockLegacyVacuum(
name="Testing",
entity_id="vacuum.test",
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
)
setup_test_component_platform(hass, VACUUM_DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
state = hass.states.get(entity.entity_id)
assert state is not None
assert state.state == "docked"
await hass.services.async_call(
VACUUM_DOMAIN,
SERVICE_START,
{
"entity_id": entity.entity_id,
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity.entity_id)
assert state is not None
assert state.state == "cleaning"

View File

@ -9,18 +9,9 @@ from homeassistant.components.vacuum import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_DOCKED,
STATE_RETURNING,
)
from homeassistant.const import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_IDLE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
VacuumActivity,
)
from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.state import async_reproduce_state
@ -39,11 +30,11 @@ async def test_reproducing_states(
hass.states.async_set(
"vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}
)
hass.states.async_set("vacuum.entity_cleaning", STATE_CLEANING, {})
hass.states.async_set("vacuum.entity_docked", STATE_DOCKED, {})
hass.states.async_set("vacuum.entity_idle", STATE_IDLE, {})
hass.states.async_set("vacuum.entity_returning", STATE_RETURNING, {})
hass.states.async_set("vacuum.entity_paused", STATE_PAUSED, {})
hass.states.async_set("vacuum.entity_cleaning", VacuumActivity.CLEANING, {})
hass.states.async_set("vacuum.entity_docked", VacuumActivity.DOCKED, {})
hass.states.async_set("vacuum.entity_idle", VacuumActivity.IDLE, {})
hass.states.async_set("vacuum.entity_returning", VacuumActivity.RETURNING, {})
hass.states.async_set("vacuum.entity_paused", VacuumActivity.PAUSED, {})
turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON)
turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF)
@ -60,11 +51,11 @@ async def test_reproducing_states(
State("vacuum.entity_off", STATE_OFF),
State("vacuum.entity_on", STATE_ON),
State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}),
State("vacuum.entity_cleaning", STATE_CLEANING),
State("vacuum.entity_docked", STATE_DOCKED),
State("vacuum.entity_idle", STATE_IDLE),
State("vacuum.entity_returning", STATE_RETURNING),
State("vacuum.entity_paused", STATE_PAUSED),
State("vacuum.entity_cleaning", VacuumActivity.CLEANING),
State("vacuum.entity_docked", VacuumActivity.DOCKED),
State("vacuum.entity_idle", VacuumActivity.IDLE),
State("vacuum.entity_returning", VacuumActivity.RETURNING),
State("vacuum.entity_paused", VacuumActivity.PAUSED),
],
)
@ -95,11 +86,11 @@ async def test_reproducing_states(
State("vacuum.entity_off", STATE_ON),
State("vacuum.entity_on", STATE_OFF),
State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_HIGH}),
State("vacuum.entity_cleaning", STATE_PAUSED),
State("vacuum.entity_docked", STATE_CLEANING),
State("vacuum.entity_idle", STATE_DOCKED),
State("vacuum.entity_returning", STATE_CLEANING),
State("vacuum.entity_paused", STATE_IDLE),
State("vacuum.entity_cleaning", VacuumActivity.PAUSED),
State("vacuum.entity_docked", VacuumActivity.CLEANING),
State("vacuum.entity_idle", VacuumActivity.DOCKED),
State("vacuum.entity_returning", VacuumActivity.CLEANING),
State("vacuum.entity_paused", VacuumActivity.IDLE),
# Should not raise
State("vacuum.non_existing", STATE_ON),
],

View File

@ -21,8 +21,7 @@ from homeassistant.components.vacuum import (
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
STATE_CLEANING,
STATE_ERROR,
VacuumActivity,
)
from homeassistant.components.xiaomi_miio.const import (
CONF_FLOW_TYPE,
@ -264,7 +263,7 @@ async def test_xiaomi_vacuum_services(
# Check state attributes
state = hass.states.get(entity_id)
assert state.state == STATE_ERROR
assert state.state == VacuumActivity.ERROR
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204
assert state.attributes.get(ATTR_ERROR) == "Error message"
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80"
@ -450,7 +449,7 @@ async def test_xiaomi_specific_services(
# Check state attributes
state = hass.states.get(entity_id)
assert state.state == STATE_CLEANING
assert state.state == VacuumActivity.CLEANING
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204
assert state.attributes.get(ATTR_ERROR) is None
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30"