From 9fd9d1f106cc7d31f97908f3d069d2bd7934f660 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 20 Sep 2024 17:37:02 +0000 Subject: [PATCH 01/86] Implement new state property for vacuum which is using an enum --- homeassistant/components/vacuum/__init__.py | 112 ++++++++++++++++++-- homeassistant/components/vacuum/const.py | 48 ++++++++- 2 files changed, 146 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index a81dbeacee1..d5442ab8c01 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -2,11 +2,13 @@ from __future__ import annotations +import asyncio from datetime import timedelta from enum import IntFlag from functools import partial +import inspect import logging -from typing import Any +from typing import Any, final from propcache import cached_property import voluptuous as vol @@ -22,8 +24,8 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API STATE_ON, STATE_PAUSED, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -32,12 +34,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.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, + STATES, + VacuumEntityState, +) _LOGGER = logging.getLogger(__name__) @@ -64,9 +75,6 @@ SERVICE_START = "start" SERVICE_PAUSE = "pause" SERVICE_STOP = "stop" - -STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] - DEFAULT_NAME = "Vacuum cleaner robot" @@ -234,8 +242,82 @@ class StateVacuumEntity( _attr_fan_speed: str | None = None _attr_fan_speed_list: list[str] _attr_state: str | None = None + _attr_vacuum_state: VacuumEntityState | None = None _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) + __vacuum_legacy_state: bool = False + __vacuum_legacy_state_reported: 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 'vacuum_state' 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 settings '_attr_state' directly + unless already reported. + """ + if __name == "_attr_state": + if self.__vacuum_legacy_state_reported is not True: + self._report_deprecated_alarm_state_handling() + self.__vacuum_legacy_state_reported = True + 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 and not self.__vacuum_legacy_state_reported: + self._report_deprecated_alarm_state_handling() + + @callback + def _report_deprecated_alarm_state_handling(self) -> None: + """Report on deprecated handling of vacuum state. + + Integrations should implement vacuum_state instead of using state directly. + """ + if self.__vacuum_legacy_state_reported is True: + return + self.__vacuum_legacy_state_reported = True + module = inspect.getmodule(self) + if module and module.__file__ and "custom_components" in module.__file__: + # Do not report on core integrations as they will be fixed. + report_issue = "report it to the custom integration author." + _LOGGER.warning( + "Entity %s (%s) is setting state directly" + " which will stop working in HA Core 2025.10." + " Entities should implement the 'vacuum_state' property and" + " return it's state using the VacuumEntityState enum, please %s", + self.entity_id, + type(self), + report_issue, + ) + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_vacuum_state_{self.platform.platform_name}", + breaks_in_ha_version="2025.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=self.platform.platform_name, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_vacuum_state", + translation_placeholders={ + "platform": self.platform.platform_name, + "report_issue": report_issue, + }, + ) + @cached_property def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" @@ -244,7 +326,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.vacuum_state == VacuumEntityState.DOCKED) return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -282,10 +364,22 @@ class StateVacuumEntity( return data + @final @cached_property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" - return self._attr_state + if (vacuum_state := self.vacuum_state) is None: + return None + return str(vacuum_state) + + @cached_property + def vacuum_state(self) -> VacuumEntityState | None: + """Return the current vacuum entity state. + + Integrations should overwrite this or use the 'attr_vacuum_state' + attribute to set the alarm status using the 'VacuumEntityState' enum. + """ + return self._attr_vacuum_state @cached_property def supported_features(self) -> VacuumEntityFeature: diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py index af1558f8570..205fbeee236 100644 --- a/homeassistant/components/vacuum/const.py +++ b/homeassistant/components/vacuum/const.py @@ -1,10 +1,48 @@ """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 VacuumEntityState(StrEnum): + """Vacuum entity states.""" + + CLEANING = "cleaning" + DOCKED = "docked" + IDLE = "idle" + PAUSED = "paused" + RETURNING = "returning" + ERROR = "error" + + +# These STATE_* constants are deprecated as of Home Assistant 2024.11. +# Please use the VacuumEntityState enum instead. +_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum( + VacuumEntityState.CLEANING, "2025.11" +) +_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumEntityState.DOCKED, "2025.11") +_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum( + VacuumEntityState.RETURNING, "2025.11" +) +_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumEntityState.ERROR, "2025.11") + + +STATES = [cls.value for cls in VacuumEntityState] + +# 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()) From 76574b5a12f3a3d52fe27ce77b603cf0e9e889dc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 12:29:23 +0000 Subject: [PATCH 02/86] Mod --- homeassistant/components/vacuum/__init__.py | 29 +++++---------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index d5442ab8c01..466d34cf9fc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -6,7 +6,6 @@ import asyncio from datetime import timedelta from enum import IntFlag from functools import partial -import inspect import logging from typing import Any, final @@ -25,7 +24,7 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API STATE_PAUSED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -224,7 +223,7 @@ STATE_VACUUM_CACHED_PROPERTIES_WITH_ATTR_ = { "battery_icon", "fan_speed", "fan_speed_list", - "state", + "vacuum_state", } @@ -252,7 +251,7 @@ class StateVacuumEntity( """Post initialisation processing.""" super().__init_subclass__(**kwargs) if any(method in cls.__dict__ for method in ("_attr_state", "state")): - # Integrations should use the 'vacuum_state' property instead of + # Integrations should use the 'alarm_state' property instead of # setting the state directly. cls.__vacuum_legacy_state = True @@ -289,34 +288,18 @@ class StateVacuumEntity( if self.__vacuum_legacy_state_reported is True: return self.__vacuum_legacy_state_reported = True - module = inspect.getmodule(self) - if module and module.__file__ and "custom_components" in module.__file__: - # Do not report on core integrations as they will be fixed. + if "custom_components" in type(self).__module__: + # Do not report on core integrations as they have been fixed. report_issue = "report it to the custom integration author." _LOGGER.warning( "Entity %s (%s) is setting state directly" - " which will stop working in HA Core 2025.10." + " which will stop working in HA Core 2025.11." " Entities should implement the 'vacuum_state' property and" " return it's state using the VacuumEntityState enum, please %s", self.entity_id, type(self), report_issue, ) - ir.async_create_issue( - self.hass, - DOMAIN, - f"deprecated_vacuum_state_{self.platform.platform_name}", - breaks_in_ha_version="2025.10.0", - is_fixable=False, - is_persistent=False, - issue_domain=self.platform.platform_name, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_vacuum_state", - translation_placeholders={ - "platform": self.platform.platform_name, - "report_issue": report_issue, - }, - ) @cached_property def battery_level(self) -> int | None: From 2c3c9f057f4736434e25c5c29de1fc17b71a953b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 12:35:48 +0000 Subject: [PATCH 03/86] Mod init --- homeassistant/components/vacuum/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 466d34cf9fc..ff4443afb06 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -348,7 +348,7 @@ class StateVacuumEntity( return data @final - @cached_property + @property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" if (vacuum_state := self.vacuum_state) is None: From 8ca4097dda9585336e532d624e821a9c455ece6f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 12:45:54 +0000 Subject: [PATCH 04/86] Mods --- homeassistant/components/vacuum/__init__.py | 7 ++++-- .../components/vacuum/device_condition.py | 6 ++--- .../components/vacuum/device_trigger.py | 6 ++--- .../components/vacuum/reproduce_state.py | 24 ++++++++----------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ff4443afb06..e8bce62815c 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -19,9 +19,7 @@ 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, callback from homeassistant.helpers import config_validation as cv @@ -76,6 +74,11 @@ SERVICE_STOP = "stop" DEFAULT_NAME = "Vacuum cleaner robot" +# These STATE_* constants are deprecated as of Home Assistant 2024.11. +# Please use the VacuumEntityState enum instead. +_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumEntityState.IDLE, "2025.11") +_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumEntityState.PAUSED, "2025.11") + class VacuumEntityFeature(IntFlag): """Supported features of the vacuum entity.""" diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index f528b0918a1..e7955f692ac 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -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, VacuumEntityState 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 = [VacuumEntityState.DOCKED] else: - test_states = [STATE_CLEANING, STATE_RETURNING] + test_states = [VacuumEntityState.CLEANING, VacuumEntityState.RETURNING] registry = er.async_get(hass) entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID]) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 45b0696f871..4d6b7d2c3c0 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -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, VacuumEntityState 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 = VacuumEntityState.CLEANING else: - to_state = STATE_DOCKED + to_state = VacuumEntityState.DOCKED state_config = { CONF_PLATFORM: "state", diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 762cd6f2e90..62fb9cd9db3 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -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, + VacuumEntityState, ) _LOGGER = logging.getLogger(__name__) VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} VALID_STATES_STATE = { - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, + VacuumEntityState.CLEANING, + VacuumEntityState.DOCKED, + VacuumEntityState.IDLE, + VacuumEntityState.PAUSED, + VacuumEntityState.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 == VacuumEntityState.CLEANING: service = SERVICE_START - elif state.state in [STATE_DOCKED, STATE_RETURNING]: + elif state.state in [VacuumEntityState.DOCKED, VacuumEntityState.RETURNING]: service = SERVICE_RETURN_TO_BASE - elif state.state == STATE_IDLE: + elif state.state == VacuumEntityState.IDLE: service = SERVICE_STOP - elif state.state == STATE_PAUSED: + elif state.state == VacuumEntityState.PAUSED: service = SERVICE_PAUSE await hass.services.async_call( From ea2055c403923bdb5133f0a9efee0c3190c21c0e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 13:10:44 +0000 Subject: [PATCH 05/86] Fix integrations --- .../components/alexa/capabilities.py | 2 +- homeassistant/components/demo/vacuum.py | 32 +++++----- homeassistant/components/ecovacs/vacuum.py | 33 +++++------ .../components/google_assistant/trait.py | 10 ++-- homeassistant/components/group/registry.py | 8 +-- .../components/homekit/type_switches.py | 4 +- .../components/litterrobot/vacuum.py | 31 +++++----- homeassistant/components/mqtt/vacuum.py | 35 ++++++----- homeassistant/components/neato/vacuum.py | 25 ++++---- homeassistant/components/roborock/vacuum.py | 55 ++++++++--------- homeassistant/components/roomba/entity.py | 48 +++++++-------- homeassistant/components/sharkiq/vacuum.py | 20 +++---- .../components/switchbot_cloud/vacuum.py | 27 ++++----- homeassistant/components/template/vacuum.py | 21 +++---- homeassistant/components/tuya/vacuum.py | 55 ++++++++--------- .../components/xiaomi_miio/vacuum.py | 59 +++++++++---------- 16 files changed, 212 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 09b461428ac..290ac6830bc 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -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.VacuumEntityState.CLEANING elif self.entity.domain == timer.DOMAIN: is_on = self.entity.state != STATE_IDLE elif self.entity.domain == water_heater.DOMAIN: diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index d4c3820d29e..16d6ab4ec33 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -7,13 +7,9 @@ from typing import Any from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, StateVacuumEntity, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -91,13 +87,13 @@ class StateDemoVacuum(StateVacuumEntity): """Initialize the vacuum.""" self._attr_name = name self._attr_supported_features = supported_features - self._state = STATE_DOCKED + self._state = VacuumEntityState.DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 @property - def state(self) -> str: + def vacuum_state(self) -> VacuumEntityState: """Return the current state of the vacuum.""" return self._state @@ -123,33 +119,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._state != VacuumEntityState.CLEANING: + self._state = VacuumEntityState.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._state == VacuumEntityState.CLEANING: + self._state = VacuumEntityState.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._state = VacuumEntityState.IDLE self.schedule_update_ha_state() def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" - self._state = STATE_RETURNING + self._state = VacuumEntityState.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._state = VacuumEntityState.CLEANING self._cleaned_area += 1.32 self._battery_level -= 1 self.schedule_update_ha_state() @@ -167,12 +163,12 @@ class StateDemoVacuum(StateVacuumEntity): "persistent_notification", service_data={"message": "I'm here!", "title": "Locate request"}, ) - self._state = STATE_IDLE + self._state = VacuumEntityState.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._state = VacuumEntityState.CLEANING self.async_write_ha_state() async def async_send_command( @@ -182,9 +178,9 @@ class StateDemoVacuum(StateVacuumEntity): **kwargs: Any, ) -> None: """Send a command to the vacuum.""" - self._state = STATE_IDLE + self._state = VacuumEntityState.IDLE self.async_write_ha_state() def __set_state_to_dock(self, _: datetime) -> None: - self._state = STATE_DOCKED + self._state = VacuumEntityState.DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 0d14267e08d..b7a690bf7d6 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -13,15 +13,10 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError @@ -123,22 +118,22 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): self.schedule_update_ha_state() @property - def state(self) -> str | None: + def vacuum_state(self) -> VacuumEntityState | None: """Return the state of the vacuum cleaner.""" if self.error is not None: - return STATE_ERROR + return VacuumEntityState.ERROR if self.device.is_cleaning: - return STATE_CLEANING + return VacuumEntityState.CLEANING if self.device.is_charging: - return STATE_DOCKED + return VacuumEntityState.DOCKED if self.device.vacuum_status == sucks.CLEAN_MODE_STOP: - return STATE_IDLE + return VacuumEntityState.IDLE if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING: - return STATE_RETURNING + return VacuumEntityState.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 == VacuumEntityState.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: VacuumEntityState.IDLE, + State.CLEANING: VacuumEntityState.CLEANING, + State.RETURNING: VacuumEntityState.RETURNING, + State.DOCKED: VacuumEntityState.DOCKED, + State.ERROR: VacuumEntityState.ERROR, + State.PAUSED: VacuumEntityState.PAUSED, } _ATTR_ROOMS = "rooms" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f99f1574038..e41725176fa 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -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.VacuumEntityState.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.VacuumEntityState.DOCKED, + "isPluggedIn": self.state.state == vacuum.VacuumEntityState.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.VacuumEntityState.CLEANING, + "isPaused": state == vacuum.VacuumEntityState.PAUSED, } if domain in COVER_VALVE_DOMAINS: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 7ac5770f171..d2f091d95c2 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -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 VacuumEntityState 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, + VacuumEntityState.CLEANING, + VacuumEntityState.RETURNING, + VacuumEntityState.ERROR, }, STATE_ON, STATE_OFF, diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 68df6c38ad6..d8e3f7bde88 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -21,8 +21,8 @@ from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START, - STATE_CLEANING, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -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 (VacuumEntityState.CLEANING, STATE_ON) _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) self.char_on.set_value(current_state) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index f5553bf5d49..baa1215b5da 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -10,13 +10,10 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( - STATE_CLEANING, - STATE_DOCKED, - STATE_ERROR, - STATE_PAUSED, StateVacuumEntity, StateVacuumEntityDescription, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform @@ -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: VacuumEntityState.CLEANING, + LitterBoxStatus.EMPTY_CYCLE: VacuumEntityState.CLEANING, + LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumEntityState.DOCKED, + LitterBoxStatus.CAT_DETECTED: VacuumEntityState.DOCKED, + LitterBoxStatus.CAT_SENSOR_TIMING: VacuumEntityState.DOCKED, + LitterBoxStatus.DRAWER_FULL_1: VacuumEntityState.DOCKED, + LitterBoxStatus.DRAWER_FULL_2: VacuumEntityState.DOCKED, + LitterBoxStatus.READY: VacuumEntityState.DOCKED, + LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumEntityState.PAUSED, + LitterBoxStatus.OFF: VacuumEntityState.DOCKED, } LITTER_BOX_ENTITY = StateVacuumEntityDescription( @@ -78,9 +75,11 @@ class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity): ) @property - def state(self) -> str: + def vacuum_state(self) -> VacuumEntityState: """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, VacuumEntityState.ERROR + ) @property def status(self) -> str: diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 86b32aa281b..72a3fc57252 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -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, VacuumEntityFeature, + VacuumEntityState, ) 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 @@ -43,13 +35,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, VacuumEntityState] = { + STATE_IDLE: VacuumEntityState.IDLE, + STATE_DOCKED: VacuumEntityState.DOCKED, + STATE_ERROR: VacuumEntityState.ERROR, + STATE_PAUSED: VacuumEntityState.PAUSED, + STATE_RETURNING: VacuumEntityState.RETURNING, + STATE_CLEANING: VacuumEntityState.CLEANING, } CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES @@ -263,7 +262,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_vacuum_state = ( POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None ) del payload[STATE] diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 77ca5346b10..1685340c2df 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -12,15 +12,12 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_STATUS, - STATE_CLEANING, - STATE_DOCKED, - STATE_ERROR, - STATE_RETURNING, StateVacuumEntity, VacuumEntityFeature, + VacuumEntityState, ) 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_state = VacuumEntityState.DOCKED self._status_state = "Charging" elif ( self._state["details"]["isDocked"] and not self._state["details"]["isCharging"] ): - self._attr_state = STATE_DOCKED + self._attr_state = VacuumEntityState.DOCKED self._status_state = "Docked" else: - self._attr_state = STATE_IDLE + self._attr_state = VacuumEntityState.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_state = VacuumEntityState.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_state = VacuumEntityState.PAUSED self._status_state = "Paused" elif self._state["state"] == 4: - self._attr_state = STATE_ERROR + self._attr_state = VacuumEntityState.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_state == VacuumEntityState.CLEANING: self.robot.pause_cleaning() - self._attr_state = STATE_RETURNING + self._attr_state = VacuumEntityState.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_state = VacuumEntityState.CLEANING try: self.robot.start_cleaning(mode, navigation, category, boundary_id) except NeatoRobotException as ex: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 3b873f259e4..d2ea3c227d6 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -8,14 +8,9 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse from homeassistant.helpers import entity_platform @@ -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: VacuumEntityState.IDLE, # "Starting" + RoborockStateCode.charger_disconnected: VacuumEntityState.IDLE, # "Charger disconnected" + RoborockStateCode.idle: VacuumEntityState.IDLE, # "Idle" + RoborockStateCode.remote_control_active: VacuumEntityState.CLEANING, # "Remote control active" + RoborockStateCode.cleaning: VacuumEntityState.CLEANING, # "Cleaning" + RoborockStateCode.returning_home: VacuumEntityState.RETURNING, # "Returning home" + RoborockStateCode.manual_mode: VacuumEntityState.CLEANING, # "Manual mode" + RoborockStateCode.charging: VacuumEntityState.DOCKED, # "Charging" + RoborockStateCode.charging_problem: VacuumEntityState.ERROR, # "Charging problem" + RoborockStateCode.paused: VacuumEntityState.PAUSED, # "Paused" + RoborockStateCode.spot_cleaning: VacuumEntityState.CLEANING, # "Spot cleaning" + RoborockStateCode.error: VacuumEntityState.ERROR, # "Error" + RoborockStateCode.shutting_down: VacuumEntityState.IDLE, # "Shutting down" + RoborockStateCode.updating: VacuumEntityState.DOCKED, # "Updating" + RoborockStateCode.docking: VacuumEntityState.RETURNING, # "Docking" + RoborockStateCode.going_to_target: VacuumEntityState.CLEANING, # "Going to target" + RoborockStateCode.zoned_cleaning: VacuumEntityState.CLEANING, # "Zoned cleaning" + RoborockStateCode.segment_cleaning: VacuumEntityState.CLEANING, # "Segment cleaning" + RoborockStateCode.emptying_the_bin: VacuumEntityState.DOCKED, # "Emptying the bin" on s7+ + RoborockStateCode.washing_the_mop: VacuumEntityState.DOCKED, # "Washing the mop" on s7maxV + RoborockStateCode.going_to_wash_the_mop: VacuumEntityState.RETURNING, # "Going to wash the mop" on s7maxV + RoborockStateCode.charging_complete: VacuumEntityState.DOCKED, # "Charging complete" + RoborockStateCode.device_offline: VacuumEntityState.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 vacuum_state(self) -> VacuumEntityState | 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) diff --git a/homeassistant/components/roomba/entity.py b/homeassistant/components/roomba/entity.py index 10c3d36de12..e4b9e7377ca 100644 --- a/homeassistant/components/roomba/entity.py +++ b/homeassistant/components/roomba/entity.py @@ -7,14 +7,11 @@ import logging from homeassistant.components.vacuum import ( ATTR_STATUS, - STATE_CLEANING, - STATE_DOCKED, - STATE_ERROR, - STATE_RETURNING, StateVacuumEntity, VacuumEntityFeature, + VacuumEntityState, ) -from homeassistant.const import ATTR_CONNECTIONS, STATE_IDLE, STATE_PAUSED +from homeassistant.const import ATTR_CONNECTIONS import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -46,16 +43,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, + "": VacuumEntityState.IDLE, + "charge": VacuumEntityState.DOCKED, + "evac": VacuumEntityState.RETURNING, # Emptying at cleanbase + "hmMidMsn": VacuumEntityState.CLEANING, # Recharging at the middle of a cycle + "hmPostMsn": VacuumEntityState.RETURNING, # Cycle finished + "hmUsrDock": VacuumEntityState.RETURNING, + "pause": VacuumEntityState.PAUSED, + "run": VacuumEntityState.CLEANING, + "stop": VacuumEntityState.IDLE, + "stuck": VacuumEntityState.ERROR, } @@ -128,7 +125,7 @@ class IRobotEntity(Entity): return dt_util.utc_from_timestamp(ts) @property - def _robot_state(self): + def _robot_state(self) -> VacuumEntityState: """Return the state of the vacuum cleaner.""" clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {}) cycle = clean_mission_status.get("cycle") @@ -136,9 +133,12 @@ class IRobotEntity(Entity): try: state = STATE_MAP[phase] except KeyError: - return STATE_ERROR - if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED): - state = STATE_PAUSED + return VacuumEntityState.ERROR + if cycle != "none" and state in ( + VacuumEntityState.IDLE, + VacuumEntityState.DOCKED, + ): + state = VacuumEntityState.PAUSED return state async def async_added_to_hass(self): @@ -169,7 +169,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1 @property - def state(self): + def vacuum_state(self) -> VacuumEntityState: """Return the state of the vacuum cleaner.""" return self._robot_state @@ -189,7 +189,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf # Only add cleaning time and cleaned area attrs when the vacuum is # currently on - if self.state == STATE_CLEANING: + if self.state == VacuumEntityState.CLEANING: # Get clean mission status ( state_attrs[ATTR_CLEANING_TIME], @@ -243,7 +243,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_start(self): """Start or resume the cleaning task.""" - if self.state == STATE_PAUSED: + if self.vacuum_state == VacuumEntityState.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") @@ -258,10 +258,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self.state == STATE_CLEANING: + if self.vacuum_state == VacuumEntityState.CLEANING: await self.async_pause() for _ in range(10): - if self.state == STATE_PAUSED: + if self.state == VacuumEntityState.PAUSED: break await asyncio.sleep(1) await self.hass.async_add_executor_job(self.vacuum.send_command, "dock") diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 8f0547980c3..bcf706ef575 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -9,13 +9,9 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -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: VacuumEntityState.PAUSED, + OperatingModes.START: VacuumEntityState.CLEANING, + OperatingModes.STOP: VacuumEntityState.IDLE, + OperatingModes.RETURN: VacuumEntityState.RETURNING, } FAN_SPEEDS_MAP = { @@ -151,7 +147,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.error_text @property - def operating_mode(self) -> str | None: + def operating_mode(self) -> VacuumEntityState | None: """Operating mode.""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @@ -162,7 +158,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @property - def state(self) -> str | None: + def vacuum_state(self) -> VacuumEntityState | None: """Get the current vacuum state. NB: Currently, we do not return an error state because they can be very, very stale. @@ -170,7 +166,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum user a notification. """ if self.sharkiq.get_property_value(Properties.CHARGING_STATUS): - return STATE_DOCKED + return VacuumEntityState.DOCKED return self.operating_mode @property diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index f9236507037..6a115fbb9cd 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -5,14 +5,9 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -44,16 +39,16 @@ 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, + "StandBy": VacuumEntityState.IDLE, + "Clearing": VacuumEntityState.CLEANING, + "Paused": VacuumEntityState.PAUSED, + "GotoChargeBase": VacuumEntityState.RETURNING, + "Charging": VacuumEntityState.DOCKED, + "ChargeDone": VacuumEntityState.DOCKED, + "Dormant": VacuumEntityState.IDLE, + "InTrouble": VacuumEntityState.ERROR, + "InRemoteControl": VacuumEntityState.CLEANING, + "InDustCollecting": VacuumEntityState.DOCKED, } VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = { diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 1d021bcb571..9db93cfcd6a 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -17,14 +17,9 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -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, + VacuumEntityState.CLEANING, + VacuumEntityState.DOCKED, + VacuumEntityState.PAUSED, + VacuumEntityState.IDLE, + VacuumEntityState.RETURNING, + VacuumEntityState.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 vacuum_state(self) -> VacuumEntityState | None: """Return the status of the vacuum cleaner.""" return self._state diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 2e0a154e670..02c5e0bdc71 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -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, VacuumEntityFeature, + VacuumEntityState, ) -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": VacuumEntityState.DOCKED, + "chargecompleted": VacuumEntityState.DOCKED, + "chargego": VacuumEntityState.DOCKED, + "charging": VacuumEntityState.DOCKED, + "cleaning": VacuumEntityState.CLEANING, + "docking": VacuumEntityState.RETURNING, + "goto_charge": VacuumEntityState.RETURNING, + "goto_pos": VacuumEntityState.CLEANING, + "mop_clean": VacuumEntityState.CLEANING, + "part_clean": VacuumEntityState.CLEANING, + "paused": VacuumEntityState.PAUSED, + "pick_zone_clean": VacuumEntityState.CLEANING, + "pos_arrived": VacuumEntityState.CLEANING, + "pos_unarrive": VacuumEntityState.CLEANING, + "random": VacuumEntityState.CLEANING, + "sleep": VacuumEntityState.IDLE, + "smart_clean": VacuumEntityState.CLEANING, + "smart": VacuumEntityState.CLEANING, + "spot_clean": VacuumEntityState.CLEANING, + "standby": VacuumEntityState.IDLE, + "wall_clean": VacuumEntityState.CLEANING, + "wall_follow": VacuumEntityState.CLEANING, + "zone_clean": VacuumEntityState.CLEANING, } @@ -137,12 +134,12 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): return self.device.status.get(DPCode.SUCTION) @property - def state(self) -> str | None: + def vacuum_state(self) -> VacuumEntityState | 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 VacuumEntityState.PAUSED if not (status := self.device.status.get(DPCode.STATUS)): return None return TUYA_STATUS_TO_HA.get(status) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index b720cc90d2c..5e45a74ff40 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -10,14 +10,9 @@ 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, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE @@ -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: VacuumEntityState.IDLE, # "Starting" + 2: VacuumEntityState.IDLE, # "Charger disconnected" + 3: VacuumEntityState.IDLE, # "Idle" + 4: VacuumEntityState.CLEANING, # "Remote control active" + 5: VacuumEntityState.CLEANING, # "Cleaning" + 6: VacuumEntityState.RETURNING, # "Returning home" + 7: VacuumEntityState.CLEANING, # "Manual mode" + 8: VacuumEntityState.DOCKED, # "Charging" + 9: VacuumEntityState.ERROR, # "Charging problem" + 10: VacuumEntityState.PAUSED, # "Paused" + 11: VacuumEntityState.CLEANING, # "Spot cleaning" + 12: VacuumEntityState.ERROR, # "Error" + 13: VacuumEntityState.IDLE, # "Shutting down" + 14: VacuumEntityState.DOCKED, # "Updating" + 15: VacuumEntityState.RETURNING, # "Docking" + 16: VacuumEntityState.CLEANING, # "Going to target" + 17: VacuumEntityState.CLEANING, # "Zoned cleaning" + 18: VacuumEntityState.CLEANING, # "Segment cleaning" + 22: VacuumEntityState.DOCKED, # "Emptying the bin" on s7+ + 23: VacuumEntityState.DOCKED, # "Washing the mop" on s7maxV + 26: VacuumEntityState.RETURNING, # "Going to wash the mop" on s7maxV + 100: VacuumEntityState.DOCKED, # "Charging complete" + 101: VacuumEntityState.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: VacuumEntityState | 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 vacuum_state(self) -> VacuumEntityState | 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 VacuumEntityState.ERROR return self._state From d638a3b6e31e68ad58daa7ff5410470412fea0d0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 13:25:36 +0000 Subject: [PATCH 06/86] Tests --- tests/components/demo/test_vacuum.py | 36 +++++------ .../components/google_assistant/test_trait.py | 12 ++-- .../components/homekit/test_type_switches.py | 7 +-- tests/components/mqtt/test_vacuum.py | 13 ++-- tests/components/sharkiq/test_vacuum.py | 15 ++--- tests/components/template/test_vacuum.py | 63 ++++++++++--------- tests/components/vacuum/__init__.py | 18 +++--- .../vacuum/test_device_condition.py | 15 ++--- .../components/vacuum/test_device_trigger.py | 16 ++--- tests/components/vacuum/test_init.py | 15 ++--- .../components/vacuum/test_reproduce_state.py | 43 +++++-------- tests/components/xiaomi_miio/test_vacuum.py | 7 +-- 12 files changed, 117 insertions(+), 143 deletions(-) diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index a4e4d6f0e1f..64c6cac5f5c 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -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, + VacuumEntityState, ) 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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.IDLE diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 1e42edf8e7b..8052ec20c01 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -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.VacuumEntityState.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.VacuumEntityState.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.VacuumEntityState.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.VacuumEntityState.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.VacuumEntityState.PAUSED, {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE}, ), BASIC_CONFIG, diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 9b708f18b8a..64358058234 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -26,9 +26,8 @@ from homeassistant.components.vacuum import ( SERVICE_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_CLEANING, - STATE_DOCKED, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -295,7 +294,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, - STATE_CLEANING, + VacuumEntityState.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, + VacuumEntityState.DOCKED, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index fef62c33a93..907ef8b5c84 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -27,8 +27,7 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, SERVICE_STOP, - STATE_CLEANING, - STATE_DOCKED, + VacuumEntityState, ) 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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.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 == VacuumEntityState.DOCKED assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60" assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 3748cfd6dc4..ed3d9715521 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -35,11 +35,8 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - STATE_CLEANING, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -160,7 +157,7 @@ async def test_simple_properties( assert entity assert state - assert state.state == STATE_CLEANING + assert state.state == VacuumEntityState.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, VacuumEntityState.IDLE), + (SERVICE_PAUSE, VacuumEntityState.PAUSED), + (SERVICE_RETURN_TO_BASE, VacuumEntityState.RETURNING), + (SERVICE_START, VacuumEntityState.CLEANING), ], ) async def test_cleaning_states( diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index ff428c5d4b4..ab7991fa948 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -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, VacuumEntityState 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, + VacuumEntityState.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, VacuumEntityState.CLEANING) hass.states.async_set(_BATTERY_LEVEL_INPUT_NUMBER, 100) await hass.async_block_till_done() - _verify(hass, STATE_CLEANING, 100) + _verify(hass, VacuumEntityState.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 == VacuumEntityState.CLEANING + _verify(hass, VacuumEntityState.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 == VacuumEntityState.PAUSED + _verify(hass, VacuumEntityState.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 == VacuumEntityState.IDLE + _verify(hass, VacuumEntityState.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 == VacuumEntityState.RETURNING + _verify(hass, VacuumEntityState.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": [VacuumEntityState.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": VacuumEntityState.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, + VacuumEntityState.CLEANING, + VacuumEntityState.DOCKED, + VacuumEntityState.IDLE, + VacuumEntityState.PAUSED, + VacuumEntityState.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": VacuumEntityState.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": VacuumEntityState.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": VacuumEntityState.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": VacuumEntityState.RETURNING, }, }, { diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index 0a681730cb2..6b8aba9cc02 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -4,13 +4,9 @@ from typing import Any from homeassistant.components.vacuum import ( DOMAIN, - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, StateVacuumEntity, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -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_state = VacuumEntityState.DOCKED self._attr_fan_speed = "slow" def stop(self, **kwargs: Any) -> None: """Stop cleaning.""" - self._attr_state = STATE_IDLE + self._attr_state = VacuumEntityState.IDLE def return_to_base(self, **kwargs: Any) -> None: """Return to base.""" - self._attr_state = STATE_RETURNING + self._attr_state = VacuumEntityState.RETURNING def clean_spot(self, **kwargs: Any) -> None: """Clean a spot.""" - self._attr_state = STATE_CLEANING + self._attr_state = VacuumEntityState.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_state = VacuumEntityState.CLEANING def pause(self) -> None: """Pause cleaning.""" - self._attr_state = STATE_PAUSED + self._attr_state = VacuumEntityState.PAUSED async def help_async_setup_entry_init( diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 9a2a67f7141..6d083bb47ba 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -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, VacuumEntityState 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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.CLEANING) assert await async_setup_component( hass, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index c186bd4d9eb..31152e749e3 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -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, VacuumEntityState 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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.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, VacuumEntityState.CLEANING) await hass.async_block_till_done() assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index d03f1d28b58..cd082f5004e 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -19,12 +19,9 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - STATE_CLEANING, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, StateVacuumEntity, VacuumEntityFeature, + VacuumEntityState, ) from homeassistant.core import HomeAssistant @@ -75,11 +72,11 @@ def test_deprecated_constants( @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, VacuumEntityState.CLEANING), + (SERVICE_PAUSE, VacuumEntityState.PAUSED), + (SERVICE_RETURN_TO_BASE, VacuumEntityState.RETURNING), + (SERVICE_START, VacuumEntityState.CLEANING), + (SERVICE_STOP, VacuumEntityState.IDLE), ], ) async def test_state_services( diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index ff8da28e98c..c83e86336f1 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -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, + VacuumEntityState, ) +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", VacuumEntityState.CLEANING, {}) + hass.states.async_set("vacuum.entity_docked", VacuumEntityState.DOCKED, {}) + hass.states.async_set("vacuum.entity_idle", VacuumEntityState.IDLE, {}) + hass.states.async_set("vacuum.entity_returning", VacuumEntityState.RETURNING, {}) + hass.states.async_set("vacuum.entity_paused", VacuumEntityState.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", VacuumEntityState.CLEANING), + State("vacuum.entity_docked", VacuumEntityState.DOCKED), + State("vacuum.entity_idle", VacuumEntityState.IDLE), + State("vacuum.entity_returning", VacuumEntityState.RETURNING), + State("vacuum.entity_paused", VacuumEntityState.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", VacuumEntityState.PAUSED), + State("vacuum.entity_docked", VacuumEntityState.CLEANING), + State("vacuum.entity_idle", VacuumEntityState.DOCKED), + State("vacuum.entity_returning", VacuumEntityState.CLEANING), + State("vacuum.entity_paused", VacuumEntityState.IDLE), # Should not raise State("vacuum.non_existing", STATE_ON), ], diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 76321a1a0a8..e2e485e785c 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -21,8 +21,7 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - STATE_CLEANING, - STATE_ERROR, + VacuumEntityState, ) 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 == VacuumEntityState.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 == VacuumEntityState.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" From 1026758abdc031732d718298af35155cc366a308 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 13:39:42 +0000 Subject: [PATCH 07/86] Fix state --- homeassistant/components/ecovacs/vacuum.py | 2 +- homeassistant/components/mqtt/vacuum.py | 2 +- homeassistant/components/neato/vacuum.py | 18 +++++++++--------- homeassistant/components/romy/vacuum.py | 12 ++++++++++-- .../components/switchbot_cloud/vacuum.py | 6 ++++-- homeassistant/components/vacuum/__init__.py | 1 - tests/components/vacuum/__init__.py | 12 ++++++------ 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index b7a690bf7d6..cd1bd3f7f2b 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -279,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_vacuum_state = _STATE_TO_VACUUM_STATE[event.state] self.async_write_ha_state() self._subscribe(self._capability.battery.event, on_battery) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 72a3fc57252..998583d28c4 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -274,7 +274,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_vacuum_state"}, ) async def _subscribe_topics(self) -> None: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 1685340c2df..47ba61c7783 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -166,23 +166,23 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): robot_alert = None if self._state["state"] == 1: if self._state["details"]["isCharging"]: - self._attr_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumEntityState.DOCKED self._status_state = "Charging" elif ( self._state["details"]["isDocked"] and not self._state["details"]["isCharging"] ): - self._attr_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumEntityState.DOCKED self._status_state = "Docked" else: - self._attr_state = VacuumEntityState.IDLE + self._attr_vacuum_state = VacuumEntityState.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 = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumEntityState.CLEANING self._status_state = ( f"{MODE.get(self._state['cleaning']['mode'])} " f"{ACTION.get(self._state['action'])}" @@ -197,10 +197,10 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): else: self._status_state = robot_alert elif self._state["state"] == 3: - self._attr_state = VacuumEntityState.PAUSED + self._attr_vacuum_state = VacuumEntityState.PAUSED self._status_state = "Paused" elif self._state["state"] == 4: - self._attr_state = VacuumEntityState.ERROR + self._attr_vacuum_state = VacuumEntityState.ERROR self._status_state = ERRORS.get(self._state["error"]) self._attr_battery_level = self._state["details"]["charge"] @@ -323,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 == VacuumEntityState.CLEANING: + if self._attr_vacuum_state == VacuumEntityState.CLEANING: self.robot.pause_cleaning() - self._attr_state = VacuumEntityState.RETURNING + self._attr_vacuum_state = VacuumEntityState.RETURNING self.robot.send_to_base() except NeatoRobotException as ex: _LOGGER.error( @@ -377,7 +377,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): "Start cleaning zone '%s' with robot %s", zone, self.entity_id ) - self._attr_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumEntityState.CLEANING try: self.robot.start_cleaning(mode, navigation, category, boundary_id) except NeatoRobotException as ex: diff --git a/homeassistant/components/romy/vacuum.py b/homeassistant/components/romy/vacuum.py index de74d371f0e..ecf2c758ae4 100644 --- a/homeassistant/components/romy/vacuum.py +++ b/homeassistant/components/romy/vacuum.py @@ -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, + VacuumEntityFeature, + VacuumEntityState, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -75,7 +79,11 @@ 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 + try: + assert self.romy.status is not None + self._attr_vacuum_state = VacuumEntityState(self.romy.status) + except (AssertionError, ValueError): + self._attr_vacuum_state = None self.async_write_ha_state() diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index 6a115fbb9cd..88f80df907f 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -38,7 +38,7 @@ async def async_setup_entry( ) -VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, str] = { +VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumEntityState] = { "StandBy": VacuumEntityState.IDLE, "Clearing": VacuumEntityState.CLEANING, "Paused": VacuumEntityState.PAUSED, @@ -109,7 +109,9 @@ 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_vacuum_state = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get( + switchbot_state + ) self.async_write_ha_state() diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index e8bce62815c..7aac5c36709 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -243,7 +243,6 @@ 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_vacuum_state: VacuumEntityState | None = None _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index 6b8aba9cc02..daab7b81420 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -35,20 +35,20 @@ class MockVacuum(MockEntity, StateVacuumEntity): def __init__(self, **values: Any) -> None: """Initialize a mock vacuum entity.""" super().__init__(**values) - self._attr_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumEntityState.DOCKED self._attr_fan_speed = "slow" def stop(self, **kwargs: Any) -> None: """Stop cleaning.""" - self._attr_state = VacuumEntityState.IDLE + self._attr_vacuum_state = VacuumEntityState.IDLE def return_to_base(self, **kwargs: Any) -> None: """Return to base.""" - self._attr_state = VacuumEntityState.RETURNING + self._attr_vacuum_state = VacuumEntityState.RETURNING def clean_spot(self, **kwargs: Any) -> None: """Clean a spot.""" - self._attr_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumEntityState.CLEANING def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the fan speed.""" @@ -56,11 +56,11 @@ class MockVacuum(MockEntity, StateVacuumEntity): def start(self) -> None: """Start cleaning.""" - self._attr_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumEntityState.CLEANING def pause(self) -> None: """Pause cleaning.""" - self._attr_state = VacuumEntityState.PAUSED + self._attr_vacuum_state = VacuumEntityState.PAUSED async def help_async_setup_entry_init( From 82aa7797661b36d02cfa24822729d7e65977f3b2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 14:19:56 +0000 Subject: [PATCH 08/86] Add vacuum tests --- tests/components/vacuum/conftest.py | 95 +++++++++++- tests/components/vacuum/test_init.py | 214 +++++++++++++++++++++++++-- 2 files changed, 296 insertions(+), 13 deletions(-) diff --git a/tests/components/vacuum/conftest.py b/tests/components/vacuum/conftest.py index d298260c575..d6c197cc56a 100644 --- a/tests/components/vacuum/conftest.py +++ b/tests/components/vacuum/conftest.py @@ -1,13 +1,28 @@ """Fixtures for Vacuum platform tests.""" from collections.abc import Generator +from unittest.mock import MagicMock 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 +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,79 @@ 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 alarm_control_panel_supported_features() -> VacuumEntityFeature: + """Return the supported features for the test alarm control panel 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 alarm control panel 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, + ), + ) + + # Unnamed sensor without device class -> no name + 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 diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index cd082f5004e..7c5c45022e2 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -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, @@ -23,16 +24,22 @@ from homeassistant.components.vacuum import ( VacuumEntityFeature, VacuumEntityState, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MockVacuum, help_async_setup_entry_init, help_async_unload_entry +from .common import async_start +from .conftest import TEST_DOMAIN from tests.common import ( MockConfigEntry, MockModule, + MockPlatform, help_test_all, import_and_test_deprecated_constant_enum, mock_integration, + mock_platform, setup_test_component_platform, ) @@ -69,6 +76,25 @@ def test_deprecated_constants( ) +@pytest.mark.parametrize( + ("enum", "constant_prefix"), _create_tuples(vacuum.VacuumEntityState, "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, "2025.11" + ) + + @pytest.mark.parametrize( ("service", "expected_state"), [ @@ -98,11 +124,13 @@ 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, @@ -129,14 +157,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, @@ -175,11 +205,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, @@ -224,11 +256,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, @@ -275,3 +309,165 @@ 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 vacuum_state doesn't log issue or raise repair.""" + state = hass.states.get(mock_vacuum_entity.entity_id) + assert state is not None + assert ( + "Entities should implement the 'vacuum_state' property and" not in caplog.text + ) + + +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.""" + + 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, + ), + ) + + class MockLegacyVacuum(MockVacuum): + """Mocked vacuum entity.""" + + @property + def state(self) -> str: + """Return the state of the entity.""" + return "disarmed" + + entity = MockLegacyVacuum() + + 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), + ) + + with patch.object( + MockLegacyVacuum, + "__module__", + "tests.custom_components.test.vacuum", + ): + 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 + + assert "Entities should implement the 'vacuum_state' property and" in caplog.text + + +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.""" + + 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, + ), + ) + + class MockLegacyVacuum(MockVacuum): + """Mocked vacuum entity.""" + + def start(self) -> None: + """Start cleaning.""" + self._attr_state = VacuumEntityState.CLEANING + + entity = MockLegacyVacuum() + + 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), + ) + + with patch.object( + MockLegacyVacuum, + "__module__", + "tests.custom_components.test.vacuum", + ): + 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 + + assert ( + "Entities should implement the 'vacuum_state' property and" not in caplog.text + ) + + with patch.object( + MockLegacyVacuum, + "__module__", + "tests.custom_components.test.vacuum", + ): + await async_start(hass, entity.entity_id) + + assert "Entities should implement the 'vacuum_state' property and" in caplog.text + caplog.clear() + with patch.object( + MockLegacyVacuum, + "__module__", + "tests.custom_components.test.vacuum", + ): + await async_start(hass, entity.entity_id) + # Test we only log once + assert ( + "Entities should implement the 'vacuum_state' property and" not in caplog.text + ) From ae64169b723726dcdc3da65c984292ae676dd4ee Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 14:30:12 +0000 Subject: [PATCH 09/86] Fix last test --- tests/components/litterrobot/test_vacuum.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 735ee6653aa..bf0d1569d0d 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -15,9 +15,7 @@ from homeassistant.components.vacuum import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_START, SERVICE_STOP, - STATE_DOCKED, - STATE_ERROR, - STATE_PAUSED, + VacuumEntityState, ) 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 == VacuumEntityState.DOCKED assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID) @@ -95,15 +93,18 @@ async def test_vacuum_with_error( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == STATE_ERROR + assert vacuum.state == VacuumEntityState.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"}, VacuumEntityState.DOCKED), + ({"isDFIFull": True}, VacuumEntityState.ERROR), + ( + {"robotCycleState": "CYCLE_VacuumEntityState.CAT_DETECT"}, + VacuumEntityState.PAUSED, + ), ], ) async def test_vacuum_states( @@ -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 == VacuumEntityState.DOCKED extra = extra or {} data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})} From 2bc1b6bddd1469fa6730e175279a0a269281ee47 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 13 Oct 2024 15:45:07 +0000 Subject: [PATCH 10/86] Litterrobot tests --- tests/components/litterrobot/test_init.py | 4 ++-- tests/components/litterrobot/test_vacuum.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 21b16097603..6430f94b9d9 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import litterrobot from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_START, - STATE_DOCKED, + VacuumEntityState, ) 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 == VacuumEntityState.DOCKED await hass.services.async_call( VACUUM_DOMAIN, diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index bf0d1569d0d..89366261a38 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -102,7 +102,7 @@ async def test_vacuum_with_error( ({"displayCode": "DC_CAT_DETECT"}, VacuumEntityState.DOCKED), ({"isDFIFull": True}, VacuumEntityState.ERROR), ( - {"robotCycleState": "CYCLE_VacuumEntityState.CAT_DETECT"}, + {"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, VacuumEntityState.PAUSED, ), ], From aef136449fc41360aa36a5444b824c311e8dacee Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Oct 2024 16:06:48 +0000 Subject: [PATCH 11/86] Fixes --- .../components/alexa/capabilities.py | 2 +- homeassistant/components/demo/vacuum.py | 28 +++++----- homeassistant/components/ecovacs/vacuum.py | 28 +++++----- .../components/google_assistant/trait.py | 10 ++-- homeassistant/components/group/registry.py | 8 +-- .../components/homekit/type_switches.py | 4 +- .../components/litterrobot/vacuum.py | 28 +++++----- homeassistant/components/mqtt/vacuum.py | 16 +++--- homeassistant/components/neato/vacuum.py | 20 +++---- homeassistant/components/roborock/vacuum.py | 50 ++++++++--------- homeassistant/components/romy/vacuum.py | 4 +- homeassistant/components/roomba/entity.py | 42 +++++++-------- homeassistant/components/sharkiq/vacuum.py | 16 +++--- .../components/switchbot_cloud/vacuum.py | 24 ++++----- homeassistant/components/template/vacuum.py | 16 +++--- homeassistant/components/tuya/vacuum.py | 52 +++++++++--------- homeassistant/components/vacuum/__init__.py | 42 ++++++--------- homeassistant/components/vacuum/const.py | 18 +++---- .../components/vacuum/device_condition.py | 6 +-- .../components/vacuum/device_trigger.py | 6 +-- .../components/vacuum/reproduce_state.py | 20 +++---- .../components/xiaomi_miio/vacuum.py | 54 +++++++++---------- 22 files changed, 239 insertions(+), 255 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 290ac6830bc..027d52be28c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -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.VacuumEntityState.CLEANING + is_on = self.entity.state == vacuum.VacuumState.CLEANING elif self.entity.domain == timer.DOMAIN: is_on = self.entity.state != STATE_IDLE elif self.entity.domain == water_heater.DOMAIN: diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 16d6ab4ec33..d5552f79f33 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -9,7 +9,7 @@ from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -87,13 +87,13 @@ class StateDemoVacuum(StateVacuumEntity): """Initialize the vacuum.""" self._attr_name = name self._attr_supported_features = supported_features - self._state = VacuumEntityState.DOCKED + self._state = VacuumState.DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 @property - def vacuum_state(self) -> VacuumEntityState: + def vacuum_state(self) -> VacuumState: """Return the current state of the vacuum.""" return self._state @@ -119,33 +119,33 @@ class StateDemoVacuum(StateVacuumEntity): def start(self) -> None: """Start or resume the cleaning task.""" - if self._state != VacuumEntityState.CLEANING: - self._state = VacuumEntityState.CLEANING + if self._state != VacuumState.CLEANING: + self._state = VacuumState.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 == VacuumEntityState.CLEANING: - self._state = VacuumEntityState.PAUSED + if self._state == VacuumState.CLEANING: + self._state = VacuumState.PAUSED self.schedule_update_ha_state() def stop(self, **kwargs: Any) -> None: """Stop the cleaning task, do not return to dock.""" - self._state = VacuumEntityState.IDLE + self._state = VacuumState.IDLE self.schedule_update_ha_state() def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" - self._state = VacuumEntityState.RETURNING + self._state = VacuumState.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 = VacuumEntityState.CLEANING + self._state = VacuumState.CLEANING self._cleaned_area += 1.32 self._battery_level -= 1 self.schedule_update_ha_state() @@ -163,12 +163,12 @@ class StateDemoVacuum(StateVacuumEntity): "persistent_notification", service_data={"message": "I'm here!", "title": "Locate request"}, ) - self._state = VacuumEntityState.IDLE + self._state = VacuumState.IDLE self.async_write_ha_state() async def async_clean_spot(self, **kwargs: Any) -> None: """Locate the vacuum's position.""" - self._state = VacuumEntityState.CLEANING + self._state = VacuumState.CLEANING self.async_write_ha_state() async def async_send_command( @@ -178,9 +178,9 @@ class StateDemoVacuum(StateVacuumEntity): **kwargs: Any, ) -> None: """Send a command to the vacuum.""" - self._state = VacuumEntityState.IDLE + self._state = VacuumState.IDLE self.async_write_ha_state() def __set_state_to_dock(self, _: datetime) -> None: - self._state = VacuumEntityState.DOCKED + self._state = VacuumState.DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index cd1bd3f7f2b..73efd7e4e3e 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -16,7 +16,7 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, StateVacuumEntityDescription, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError @@ -118,22 +118,22 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): self.schedule_update_ha_state() @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | None: """Return the state of the vacuum cleaner.""" if self.error is not None: - return VacuumEntityState.ERROR + return VacuumState.ERROR if self.device.is_cleaning: - return VacuumEntityState.CLEANING + return VacuumState.CLEANING if self.device.is_charging: - return VacuumEntityState.DOCKED + return VacuumState.DOCKED if self.device.vacuum_status == sucks.CLEAN_MODE_STOP: - return VacuumEntityState.IDLE + return VacuumState.IDLE if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING: - return VacuumEntityState.RETURNING + return VacuumState.RETURNING return None @@ -197,7 +197,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - if self.state == VacuumEntityState.CLEANING: + if self.state == VacuumState.CLEANING: self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command( @@ -220,12 +220,12 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): _STATE_TO_VACUUM_STATE = { - State.IDLE: VacuumEntityState.IDLE, - State.CLEANING: VacuumEntityState.CLEANING, - State.RETURNING: VacuumEntityState.RETURNING, - State.DOCKED: VacuumEntityState.DOCKED, - State.ERROR: VacuumEntityState.ERROR, - State.PAUSED: VacuumEntityState.PAUSED, + State.IDLE: VacuumState.IDLE, + State.CLEANING: VacuumState.CLEANING, + State.RETURNING: VacuumState.RETURNING, + State.DOCKED: VacuumState.DOCKED, + State.ERROR: VacuumState.ERROR, + State.PAUSED: VacuumState.PAUSED, } _ATTR_ROOMS = "rooms" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index e41725176fa..899f0e3d8c7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -729,7 +729,7 @@ class DockTrait(_Trait): def query_attributes(self) -> dict[str, Any]: """Return dock query attributes.""" - return {"isDocked": self.state.state == vacuum.VacuumEntityState.DOCKED} + return {"isDocked": self.state.state == vacuum.VacuumState.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.VacuumEntityState.DOCKED, - "isPluggedIn": self.state.state == vacuum.VacuumEntityState.DOCKED, + "isCharging": self.state.state == vacuum.VacuumState.DOCKED, + "isPluggedIn": self.state.state == vacuum.VacuumState.DOCKED, } async def execute(self, command, data, params, challenge): @@ -882,8 +882,8 @@ class StartStopTrait(_Trait): if domain == vacuum.DOMAIN: return { - "isRunning": state == vacuum.VacuumEntityState.CLEANING, - "isPaused": state == vacuum.VacuumEntityState.PAUSED, + "isRunning": state == vacuum.VacuumState.CLEANING, + "isPaused": state == vacuum.VacuumState.PAUSED, } if domain in COVER_VALVE_DOMAINS: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index d2f091d95c2..38fe29fd253 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -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 VacuumEntityState +from homeassistant.components.vacuum import VacuumState 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, - VacuumEntityState.CLEANING, - VacuumEntityState.RETURNING, - VacuumEntityState.ERROR, + VacuumState.CLEANING, + VacuumState.RETURNING, + VacuumState.ERROR, }, STATE_ON, STATE_OFF, diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index d8e3f7bde88..fa2ac2cc404 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -22,7 +22,7 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -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 (VacuumEntityState.CLEANING, STATE_ON) + current_state = new_state.state in (VacuumState.CLEANING, STATE_ON) _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) self.char_on.set_value(current_state) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index baa1215b5da..0d1f32f4ac5 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -13,7 +13,7 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, StateVacuumEntityDescription, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform @@ -26,16 +26,16 @@ from .entity import LitterRobotEntity SERVICE_SET_SLEEP_MODE = "set_sleep_mode" LITTER_BOX_STATUS_STATE_MAP = { - LitterBoxStatus.CLEAN_CYCLE: VacuumEntityState.CLEANING, - LitterBoxStatus.EMPTY_CYCLE: VacuumEntityState.CLEANING, - LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumEntityState.DOCKED, - LitterBoxStatus.CAT_DETECTED: VacuumEntityState.DOCKED, - LitterBoxStatus.CAT_SENSOR_TIMING: VacuumEntityState.DOCKED, - LitterBoxStatus.DRAWER_FULL_1: VacuumEntityState.DOCKED, - LitterBoxStatus.DRAWER_FULL_2: VacuumEntityState.DOCKED, - LitterBoxStatus.READY: VacuumEntityState.DOCKED, - LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumEntityState.PAUSED, - LitterBoxStatus.OFF: VacuumEntityState.DOCKED, + LitterBoxStatus.CLEAN_CYCLE: VacuumState.CLEANING, + LitterBoxStatus.EMPTY_CYCLE: VacuumState.CLEANING, + LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumState.DOCKED, + LitterBoxStatus.CAT_DETECTED: VacuumState.DOCKED, + LitterBoxStatus.CAT_SENSOR_TIMING: VacuumState.DOCKED, + LitterBoxStatus.DRAWER_FULL_1: VacuumState.DOCKED, + LitterBoxStatus.DRAWER_FULL_2: VacuumState.DOCKED, + LitterBoxStatus.READY: VacuumState.DOCKED, + LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumState.PAUSED, + LitterBoxStatus.OFF: VacuumState.DOCKED, } LITTER_BOX_ENTITY = StateVacuumEntityDescription( @@ -75,11 +75,9 @@ class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity): ) @property - def vacuum_state(self) -> VacuumEntityState: + def vacuum_state(self) -> VacuumState: """Return the state of the cleaner.""" - return LITTER_BOX_STATUS_STATE_MAP.get( - self.robot.status, VacuumEntityState.ERROR - ) + return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, VacuumState.ERROR) @property def status(self) -> str: diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 998583d28c4..9891e844f74 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -12,7 +12,7 @@ from homeassistant.components.vacuum import ( ENTITY_ID_FORMAT, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME @@ -42,13 +42,13 @@ STATE_PAUSED = "paused" STATE_RETURNING = "returning" STATE_CLEANING = "cleaning" -POSSIBLE_STATES: dict[str, VacuumEntityState] = { - STATE_IDLE: VacuumEntityState.IDLE, - STATE_DOCKED: VacuumEntityState.DOCKED, - STATE_ERROR: VacuumEntityState.ERROR, - STATE_PAUSED: VacuumEntityState.PAUSED, - STATE_RETURNING: VacuumEntityState.RETURNING, - STATE_CLEANING: VacuumEntityState.CLEANING, +POSSIBLE_STATES: dict[str, VacuumState] = { + STATE_IDLE: VacuumState.IDLE, + STATE_DOCKED: VacuumState.DOCKED, + STATE_ERROR: VacuumState.ERROR, + STATE_PAUSED: VacuumState.PAUSED, + STATE_RETURNING: VacuumState.RETURNING, + STATE_CLEANING: VacuumState.CLEANING, } CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 47ba61c7783..ae1b93346b9 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -14,7 +14,7 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE @@ -166,23 +166,23 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): robot_alert = None if self._state["state"] == 1: if self._state["details"]["isCharging"]: - self._attr_vacuum_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumState.DOCKED self._status_state = "Charging" elif ( self._state["details"]["isDocked"] and not self._state["details"]["isCharging"] ): - self._attr_vacuum_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumState.DOCKED self._status_state = "Docked" else: - self._attr_vacuum_state = VacuumEntityState.IDLE + self._attr_vacuum_state = VacuumState.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_vacuum_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumState.CLEANING self._status_state = ( f"{MODE.get(self._state['cleaning']['mode'])} " f"{ACTION.get(self._state['action'])}" @@ -197,10 +197,10 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): else: self._status_state = robot_alert elif self._state["state"] == 3: - self._attr_vacuum_state = VacuumEntityState.PAUSED + self._attr_vacuum_state = VacuumState.PAUSED self._status_state = "Paused" elif self._state["state"] == 4: - self._attr_vacuum_state = VacuumEntityState.ERROR + self._attr_vacuum_state = VacuumState.ERROR self._status_state = ERRORS.get(self._state["error"]) self._attr_battery_level = self._state["details"]["charge"] @@ -323,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_vacuum_state == VacuumEntityState.CLEANING: + if self._attr_vacuum_state == VacuumState.CLEANING: self.robot.pause_cleaning() - self._attr_vacuum_state = VacuumEntityState.RETURNING + self._attr_vacuum_state = VacuumState.RETURNING self.robot.send_to_base() except NeatoRobotException as ex: _LOGGER.error( @@ -377,7 +377,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): "Start cleaning zone '%s' with robot %s", zone, self.entity_id ) - self._attr_vacuum_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumState.CLEANING try: self.robot.start_cleaning(mode, navigation, category, boundary_id) except NeatoRobotException as ex: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index d2ea3c227d6..457152ad01c 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -10,7 +10,7 @@ from roborock.roborock_typing import RoborockCommand from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse from homeassistant.helpers import entity_platform @@ -22,29 +22,29 @@ from .coordinator import RoborockDataUpdateCoordinator from .entity import RoborockCoordinatedEntityV1 STATE_CODE_TO_STATE = { - RoborockStateCode.starting: VacuumEntityState.IDLE, # "Starting" - RoborockStateCode.charger_disconnected: VacuumEntityState.IDLE, # "Charger disconnected" - RoborockStateCode.idle: VacuumEntityState.IDLE, # "Idle" - RoborockStateCode.remote_control_active: VacuumEntityState.CLEANING, # "Remote control active" - RoborockStateCode.cleaning: VacuumEntityState.CLEANING, # "Cleaning" - RoborockStateCode.returning_home: VacuumEntityState.RETURNING, # "Returning home" - RoborockStateCode.manual_mode: VacuumEntityState.CLEANING, # "Manual mode" - RoborockStateCode.charging: VacuumEntityState.DOCKED, # "Charging" - RoborockStateCode.charging_problem: VacuumEntityState.ERROR, # "Charging problem" - RoborockStateCode.paused: VacuumEntityState.PAUSED, # "Paused" - RoborockStateCode.spot_cleaning: VacuumEntityState.CLEANING, # "Spot cleaning" - RoborockStateCode.error: VacuumEntityState.ERROR, # "Error" - RoborockStateCode.shutting_down: VacuumEntityState.IDLE, # "Shutting down" - RoborockStateCode.updating: VacuumEntityState.DOCKED, # "Updating" - RoborockStateCode.docking: VacuumEntityState.RETURNING, # "Docking" - RoborockStateCode.going_to_target: VacuumEntityState.CLEANING, # "Going to target" - RoborockStateCode.zoned_cleaning: VacuumEntityState.CLEANING, # "Zoned cleaning" - RoborockStateCode.segment_cleaning: VacuumEntityState.CLEANING, # "Segment cleaning" - RoborockStateCode.emptying_the_bin: VacuumEntityState.DOCKED, # "Emptying the bin" on s7+ - RoborockStateCode.washing_the_mop: VacuumEntityState.DOCKED, # "Washing the mop" on s7maxV - RoborockStateCode.going_to_wash_the_mop: VacuumEntityState.RETURNING, # "Going to wash the mop" on s7maxV - RoborockStateCode.charging_complete: VacuumEntityState.DOCKED, # "Charging complete" - RoborockStateCode.device_offline: VacuumEntityState.ERROR, # "Device offline" + RoborockStateCode.starting: VacuumState.IDLE, # "Starting" + RoborockStateCode.charger_disconnected: VacuumState.IDLE, # "Charger disconnected" + RoborockStateCode.idle: VacuumState.IDLE, # "Idle" + RoborockStateCode.remote_control_active: VacuumState.CLEANING, # "Remote control active" + RoborockStateCode.cleaning: VacuumState.CLEANING, # "Cleaning" + RoborockStateCode.returning_home: VacuumState.RETURNING, # "Returning home" + RoborockStateCode.manual_mode: VacuumState.CLEANING, # "Manual mode" + RoborockStateCode.charging: VacuumState.DOCKED, # "Charging" + RoborockStateCode.charging_problem: VacuumState.ERROR, # "Charging problem" + RoborockStateCode.paused: VacuumState.PAUSED, # "Paused" + RoborockStateCode.spot_cleaning: VacuumState.CLEANING, # "Spot cleaning" + RoborockStateCode.error: VacuumState.ERROR, # "Error" + RoborockStateCode.shutting_down: VacuumState.IDLE, # "Shutting down" + RoborockStateCode.updating: VacuumState.DOCKED, # "Updating" + RoborockStateCode.docking: VacuumState.RETURNING, # "Docking" + RoborockStateCode.going_to_target: VacuumState.CLEANING, # "Going to target" + RoborockStateCode.zoned_cleaning: VacuumState.CLEANING, # "Zoned cleaning" + RoborockStateCode.segment_cleaning: VacuumState.CLEANING, # "Segment cleaning" + RoborockStateCode.emptying_the_bin: VacuumState.DOCKED, # "Emptying the bin" on s7+ + RoborockStateCode.washing_the_mop: VacuumState.DOCKED, # "Washing the mop" on s7maxV + RoborockStateCode.going_to_wash_the_mop: VacuumState.RETURNING, # "Going to wash the mop" on s7maxV + RoborockStateCode.charging_complete: VacuumState.DOCKED, # "Charging complete" + RoborockStateCode.device_offline: VacuumState.ERROR, # "Device offline" } @@ -107,7 +107,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): self._attr_fan_speed_list = self._device_status.fan_power_options @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | 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) diff --git a/homeassistant/components/romy/vacuum.py b/homeassistant/components/romy/vacuum.py index ecf2c758ae4..eff335cbd81 100644 --- a/homeassistant/components/romy/vacuum.py +++ b/homeassistant/components/romy/vacuum.py @@ -9,7 +9,7 @@ from typing import Any from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -81,7 +81,7 @@ class RomyVacuumEntity(RomyEntity, StateVacuumEntity): self._attr_battery_level = self.romy.battery_level try: assert self.romy.status is not None - self._attr_vacuum_state = VacuumEntityState(self.romy.status) + self._attr_vacuum_state = VacuumState(self.romy.status) except (AssertionError, ValueError): self._attr_vacuum_state = None diff --git a/homeassistant/components/roomba/entity.py b/homeassistant/components/roomba/entity.py index e4b9e7377ca..47d1072c534 100644 --- a/homeassistant/components/roomba/entity.py +++ b/homeassistant/components/roomba/entity.py @@ -9,7 +9,7 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ATTR_CONNECTIONS import homeassistant.helpers.device_registry as dr @@ -43,16 +43,16 @@ SUPPORT_IROBOT = ( ) STATE_MAP = { - "": VacuumEntityState.IDLE, - "charge": VacuumEntityState.DOCKED, - "evac": VacuumEntityState.RETURNING, # Emptying at cleanbase - "hmMidMsn": VacuumEntityState.CLEANING, # Recharging at the middle of a cycle - "hmPostMsn": VacuumEntityState.RETURNING, # Cycle finished - "hmUsrDock": VacuumEntityState.RETURNING, - "pause": VacuumEntityState.PAUSED, - "run": VacuumEntityState.CLEANING, - "stop": VacuumEntityState.IDLE, - "stuck": VacuumEntityState.ERROR, + "": VacuumState.IDLE, + "charge": VacuumState.DOCKED, + "evac": VacuumState.RETURNING, # Emptying at cleanbase + "hmMidMsn": VacuumState.CLEANING, # Recharging at the middle of a cycle + "hmPostMsn": VacuumState.RETURNING, # Cycle finished + "hmUsrDock": VacuumState.RETURNING, + "pause": VacuumState.PAUSED, + "run": VacuumState.CLEANING, + "stop": VacuumState.IDLE, + "stuck": VacuumState.ERROR, } @@ -125,7 +125,7 @@ class IRobotEntity(Entity): return dt_util.utc_from_timestamp(ts) @property - def _robot_state(self) -> VacuumEntityState: + def _robot_state(self) -> VacuumState: """Return the state of the vacuum cleaner.""" clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {}) cycle = clean_mission_status.get("cycle") @@ -133,12 +133,12 @@ class IRobotEntity(Entity): try: state = STATE_MAP[phase] except KeyError: - return VacuumEntityState.ERROR + return VacuumState.ERROR if cycle != "none" and state in ( - VacuumEntityState.IDLE, - VacuumEntityState.DOCKED, + VacuumState.IDLE, + VacuumState.DOCKED, ): - state = VacuumEntityState.PAUSED + state = VacuumState.PAUSED return state async def async_added_to_hass(self): @@ -169,7 +169,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1 @property - def vacuum_state(self) -> VacuumEntityState: + def vacuum_state(self) -> VacuumState: """Return the state of the vacuum cleaner.""" return self._robot_state @@ -189,7 +189,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf # Only add cleaning time and cleaned area attrs when the vacuum is # currently on - if self.state == VacuumEntityState.CLEANING: + if self.state == VacuumState.CLEANING: # Get clean mission status ( state_attrs[ATTR_CLEANING_TIME], @@ -243,7 +243,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_start(self): """Start or resume the cleaning task.""" - if self.vacuum_state == VacuumEntityState.PAUSED: + if self.vacuum_state == VacuumState.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") @@ -258,10 +258,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self.vacuum_state == VacuumEntityState.CLEANING: + if self.vacuum_state == VacuumState.CLEANING: await self.async_pause() for _ in range(10): - if self.state == VacuumEntityState.PAUSED: + if self.state == VacuumState.PAUSED: break await asyncio.sleep(1) await self.hass.async_add_executor_job(self.vacuum.send_command, "dock") diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index bcf706ef575..db0f5bba9d6 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -26,10 +26,10 @@ from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK from .coordinator import SharkIqUpdateCoordinator OPERATING_STATE_MAP = { - OperatingModes.PAUSE: VacuumEntityState.PAUSED, - OperatingModes.START: VacuumEntityState.CLEANING, - OperatingModes.STOP: VacuumEntityState.IDLE, - OperatingModes.RETURN: VacuumEntityState.RETURNING, + OperatingModes.PAUSE: VacuumState.PAUSED, + OperatingModes.START: VacuumState.CLEANING, + OperatingModes.STOP: VacuumState.IDLE, + OperatingModes.RETURN: VacuumState.RETURNING, } FAN_SPEEDS_MAP = { @@ -147,7 +147,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.error_text @property - def operating_mode(self) -> VacuumEntityState | None: + def operating_mode(self) -> VacuumState | None: """Operating mode.""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @@ -158,7 +158,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | None: """Get the current vacuum state. NB: Currently, we do not return an error state because they can be very, very stale. @@ -166,7 +166,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum user a notification. """ if self.sharkiq.get_property_value(Properties.CHARGING_STATUS): - return VacuumEntityState.DOCKED + return VacuumState.DOCKED return self.operating_mode @property diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index 88f80df907f..94231a83fb5 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -7,7 +7,7 @@ from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -38,17 +38,17 @@ async def async_setup_entry( ) -VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumEntityState] = { - "StandBy": VacuumEntityState.IDLE, - "Clearing": VacuumEntityState.CLEANING, - "Paused": VacuumEntityState.PAUSED, - "GotoChargeBase": VacuumEntityState.RETURNING, - "Charging": VacuumEntityState.DOCKED, - "ChargeDone": VacuumEntityState.DOCKED, - "Dormant": VacuumEntityState.IDLE, - "InTrouble": VacuumEntityState.ERROR, - "InRemoteControl": VacuumEntityState.CLEANING, - "InDustCollecting": VacuumEntityState.DOCKED, +VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumState] = { + "StandBy": VacuumState.IDLE, + "Clearing": VacuumState.CLEANING, + "Paused": VacuumState.PAUSED, + "GotoChargeBase": VacuumState.RETURNING, + "Charging": VacuumState.DOCKED, + "ChargeDone": VacuumState.DOCKED, + "Dormant": VacuumState.IDLE, + "InTrouble": VacuumState.ERROR, + "InRemoteControl": VacuumState.CLEANING, + "InDustCollecting": VacuumState.DOCKED, } VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = { diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 9db93cfcd6a..62d55c2375e 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -19,7 +19,7 @@ from homeassistant.components.vacuum import ( SERVICE_STOP, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -53,12 +53,12 @@ CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" _VALID_STATES = [ - VacuumEntityState.CLEANING, - VacuumEntityState.DOCKED, - VacuumEntityState.PAUSED, - VacuumEntityState.IDLE, - VacuumEntityState.RETURNING, - VacuumEntityState.ERROR, + VacuumState.CLEANING, + VacuumState.DOCKED, + VacuumState.PAUSED, + VacuumState.IDLE, + VacuumState.RETURNING, + VacuumState.ERROR, ] VACUUM_SCHEMA = vol.All( @@ -197,7 +197,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | None: """Return the status of the vacuum cleaner.""" return self._state diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 02c5e0bdc71..0a9429f98ae 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -9,7 +9,7 @@ from tuya_sharing import CustomerDevice, Manager from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,29 +21,29 @@ from .entity import EnumTypeData, IntegerTypeData, TuyaEntity TUYA_MODE_RETURN_HOME = "chargego" TUYA_STATUS_TO_HA = { - "charge_done": VacuumEntityState.DOCKED, - "chargecompleted": VacuumEntityState.DOCKED, - "chargego": VacuumEntityState.DOCKED, - "charging": VacuumEntityState.DOCKED, - "cleaning": VacuumEntityState.CLEANING, - "docking": VacuumEntityState.RETURNING, - "goto_charge": VacuumEntityState.RETURNING, - "goto_pos": VacuumEntityState.CLEANING, - "mop_clean": VacuumEntityState.CLEANING, - "part_clean": VacuumEntityState.CLEANING, - "paused": VacuumEntityState.PAUSED, - "pick_zone_clean": VacuumEntityState.CLEANING, - "pos_arrived": VacuumEntityState.CLEANING, - "pos_unarrive": VacuumEntityState.CLEANING, - "random": VacuumEntityState.CLEANING, - "sleep": VacuumEntityState.IDLE, - "smart_clean": VacuumEntityState.CLEANING, - "smart": VacuumEntityState.CLEANING, - "spot_clean": VacuumEntityState.CLEANING, - "standby": VacuumEntityState.IDLE, - "wall_clean": VacuumEntityState.CLEANING, - "wall_follow": VacuumEntityState.CLEANING, - "zone_clean": VacuumEntityState.CLEANING, + "charge_done": VacuumState.DOCKED, + "chargecompleted": VacuumState.DOCKED, + "chargego": VacuumState.DOCKED, + "charging": VacuumState.DOCKED, + "cleaning": VacuumState.CLEANING, + "docking": VacuumState.RETURNING, + "goto_charge": VacuumState.RETURNING, + "goto_pos": VacuumState.CLEANING, + "mop_clean": VacuumState.CLEANING, + "part_clean": VacuumState.CLEANING, + "paused": VacuumState.PAUSED, + "pick_zone_clean": VacuumState.CLEANING, + "pos_arrived": VacuumState.CLEANING, + "pos_unarrive": VacuumState.CLEANING, + "random": VacuumState.CLEANING, + "sleep": VacuumState.IDLE, + "smart_clean": VacuumState.CLEANING, + "smart": VacuumState.CLEANING, + "spot_clean": VacuumState.CLEANING, + "standby": VacuumState.IDLE, + "wall_clean": VacuumState.CLEANING, + "wall_follow": VacuumState.CLEANING, + "zone_clean": VacuumState.CLEANING, } @@ -134,12 +134,12 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): return self.device.status.get(DPCode.SUCTION) @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | None: """Return Tuya vacuum device state.""" if self.device.status.get(DPCode.PAUSE) and not ( self.device.status.get(DPCode.STATUS) ): - return VacuumEntityState.PAUSED + return VacuumState.PAUSED if not (status := self.device.status.get(DPCode.STATUS)): return None return TUYA_STATUS_TO_HA.get(status) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 7aac5c36709..0d2af17f425 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -44,7 +44,7 @@ from .const import ( # noqa: F401 _DEPRECATED_STATE_RETURNING, DOMAIN, STATES, - VacuumEntityState, + VacuumState, ) _LOGGER = logging.getLogger(__name__) @@ -75,9 +75,9 @@ SERVICE_STOP = "stop" DEFAULT_NAME = "Vacuum cleaner robot" # These STATE_* constants are deprecated as of Home Assistant 2024.11. -# Please use the VacuumEntityState enum instead. -_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumEntityState.IDLE, "2025.11") -_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumEntityState.PAUSED, "2025.11") +# Please use the VacuumState enum instead. +_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumState.IDLE, "2025.11") +_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumState.PAUSED, "2025.11") class VacuumEntityFeature(IntFlag): @@ -243,29 +243,21 @@ class StateVacuumEntity( _attr_battery_level: int | None = None _attr_fan_speed: str | None = None _attr_fan_speed_list: list[str] - _attr_vacuum_state: VacuumEntityState | None = None + _attr_vacuum_state: VacuumState | None = None _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) __vacuum_legacy_state: bool = False __vacuum_legacy_state_reported: 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 'alarm_state' 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 settings '_attr_state' directly + Deprecation warning if setting '_attr_state' directly unless already reported. """ if __name == "_attr_state": if self.__vacuum_legacy_state_reported is not True: - self._report_deprecated_alarm_state_handling() + self._report_deprecated_vacuum_state_handling() self.__vacuum_legacy_state_reported = True return super().__setattr__(__name, __value) @@ -279,16 +271,14 @@ class StateVacuumEntity( """Start adding an entity to a platform.""" super().add_to_platform_start(hass, platform, parallel_updates) if self.__vacuum_legacy_state and not self.__vacuum_legacy_state_reported: - self._report_deprecated_alarm_state_handling() + self._report_deprecated_vacuum_state_handling() @callback - def _report_deprecated_alarm_state_handling(self) -> None: + def _report_deprecated_vacuum_state_handling(self) -> None: """Report on deprecated handling of vacuum state. Integrations should implement vacuum_state instead of using state directly. """ - if self.__vacuum_legacy_state_reported is True: - return self.__vacuum_legacy_state_reported = True if "custom_components" in type(self).__module__: # Do not report on core integrations as they have been fixed. @@ -297,7 +287,7 @@ class StateVacuumEntity( "Entity %s (%s) is setting state directly" " which will stop working in HA Core 2025.11." " Entities should implement the 'vacuum_state' property and" - " return it's state using the VacuumEntityState enum, please %s", + " return its state using the VacuumState enum, please %s", self.entity_id, type(self), report_issue, @@ -311,7 +301,7 @@ class StateVacuumEntity( @property def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner.""" - charging = bool(self.vacuum_state == VacuumEntityState.DOCKED) + charging = bool(self.vacuum_state == VacuumState.DOCKED) return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -355,14 +345,14 @@ class StateVacuumEntity( """Return the state of the vacuum cleaner.""" if (vacuum_state := self.vacuum_state) is None: return None - return str(vacuum_state) + return vacuum_state @cached_property - def vacuum_state(self) -> VacuumEntityState | None: - """Return the current vacuum entity state. + def vacuum_state(self) -> VacuumState | None: + """Return the current vacuum state. - Integrations should overwrite this or use the 'attr_vacuum_state' - attribute to set the alarm status using the 'VacuumEntityState' enum. + Integrations should overwrite this or use the '_attr_vacuum_state' + attribute to set the vacuum status using the 'VacuumState' enum. """ return self._attr_vacuum_state diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py index 205fbeee236..05b631b5afe 100644 --- a/homeassistant/components/vacuum/const.py +++ b/homeassistant/components/vacuum/const.py @@ -15,7 +15,7 @@ from homeassistant.helpers.deprecation import ( DOMAIN = "vacuum" -class VacuumEntityState(StrEnum): +class VacuumState(StrEnum): """Vacuum entity states.""" CLEANING = "cleaning" @@ -27,18 +27,14 @@ class VacuumEntityState(StrEnum): # These STATE_* constants are deprecated as of Home Assistant 2024.11. -# Please use the VacuumEntityState enum instead. -_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum( - VacuumEntityState.CLEANING, "2025.11" -) -_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumEntityState.DOCKED, "2025.11") -_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum( - VacuumEntityState.RETURNING, "2025.11" -) -_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumEntityState.ERROR, "2025.11") +# Please use the VacuumState enum instead. +_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumState.CLEANING, "2025.11") +_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumState.DOCKED, "2025.11") +_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum(VacuumState.RETURNING, "2025.11") +_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumState.ERROR, "2025.11") -STATES = [cls.value for cls in VacuumEntityState] +STATES = [cls.value for cls in VacuumState] # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index e7955f692ac..072136b5a4a 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -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, VacuumEntityState +from . import DOMAIN, VacuumState 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 = [VacuumEntityState.DOCKED] + test_states = [VacuumState.DOCKED] else: - test_states = [VacuumEntityState.CLEANING, VacuumEntityState.RETURNING] + test_states = [VacuumState.CLEANING, VacuumState.RETURNING] registry = er.async_get(hass) entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID]) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 4d6b7d2c3c0..ec4238674af 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -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, VacuumEntityState +from . import DOMAIN, VacuumState 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 = VacuumEntityState.CLEANING + to_state = VacuumState.CLEANING else: - to_state = VacuumEntityState.DOCKED + to_state = VacuumState.DOCKED state_config = { CONF_PLATFORM: "state", diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 62fb9cd9db3..dde9d6ca980 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -24,18 +24,18 @@ from . import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumEntityState, + VacuumState, ) _LOGGER = logging.getLogger(__name__) VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} VALID_STATES_STATE = { - VacuumEntityState.CLEANING, - VacuumEntityState.DOCKED, - VacuumEntityState.IDLE, - VacuumEntityState.PAUSED, - VacuumEntityState.RETURNING, + VacuumState.CLEANING, + VacuumState.DOCKED, + VacuumState.IDLE, + VacuumState.PAUSED, + VacuumState.RETURNING, } @@ -71,13 +71,13 @@ async def _async_reproduce_state( service = SERVICE_TURN_ON elif state.state == STATE_OFF: service = SERVICE_TURN_OFF - elif state.state == VacuumEntityState.CLEANING: + elif state.state == VacuumState.CLEANING: service = SERVICE_START - elif state.state in [VacuumEntityState.DOCKED, VacuumEntityState.RETURNING]: + elif state.state in [VacuumState.DOCKED, VacuumState.RETURNING]: service = SERVICE_RETURN_TO_BASE - elif state.state == VacuumEntityState.IDLE: + elif state.state == VacuumState.IDLE: service = SERVICE_STOP - elif state.state == VacuumEntityState.PAUSED: + elif state.state == VacuumState.PAUSED: service = SERVICE_PAUSE await hass.services.async_call( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 5e45a74ff40..1b5fed2b7d3 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE @@ -50,29 +50,29 @@ ATTR_ZONE_REPEATER = "repeats" ATTR_TIMERS = "timers" STATE_CODE_TO_STATE = { - 1: VacuumEntityState.IDLE, # "Starting" - 2: VacuumEntityState.IDLE, # "Charger disconnected" - 3: VacuumEntityState.IDLE, # "Idle" - 4: VacuumEntityState.CLEANING, # "Remote control active" - 5: VacuumEntityState.CLEANING, # "Cleaning" - 6: VacuumEntityState.RETURNING, # "Returning home" - 7: VacuumEntityState.CLEANING, # "Manual mode" - 8: VacuumEntityState.DOCKED, # "Charging" - 9: VacuumEntityState.ERROR, # "Charging problem" - 10: VacuumEntityState.PAUSED, # "Paused" - 11: VacuumEntityState.CLEANING, # "Spot cleaning" - 12: VacuumEntityState.ERROR, # "Error" - 13: VacuumEntityState.IDLE, # "Shutting down" - 14: VacuumEntityState.DOCKED, # "Updating" - 15: VacuumEntityState.RETURNING, # "Docking" - 16: VacuumEntityState.CLEANING, # "Going to target" - 17: VacuumEntityState.CLEANING, # "Zoned cleaning" - 18: VacuumEntityState.CLEANING, # "Segment cleaning" - 22: VacuumEntityState.DOCKED, # "Emptying the bin" on s7+ - 23: VacuumEntityState.DOCKED, # "Washing the mop" on s7maxV - 26: VacuumEntityState.RETURNING, # "Going to wash the mop" on s7maxV - 100: VacuumEntityState.DOCKED, # "Charging complete" - 101: VacuumEntityState.ERROR, # "Device offline" + 1: VacuumState.IDLE, # "Starting" + 2: VacuumState.IDLE, # "Charger disconnected" + 3: VacuumState.IDLE, # "Idle" + 4: VacuumState.CLEANING, # "Remote control active" + 5: VacuumState.CLEANING, # "Cleaning" + 6: VacuumState.RETURNING, # "Returning home" + 7: VacuumState.CLEANING, # "Manual mode" + 8: VacuumState.DOCKED, # "Charging" + 9: VacuumState.ERROR, # "Charging problem" + 10: VacuumState.PAUSED, # "Paused" + 11: VacuumState.CLEANING, # "Spot cleaning" + 12: VacuumState.ERROR, # "Error" + 13: VacuumState.IDLE, # "Shutting down" + 14: VacuumState.DOCKED, # "Updating" + 15: VacuumState.RETURNING, # "Docking" + 16: VacuumState.CLEANING, # "Going to target" + 17: VacuumState.CLEANING, # "Zoned cleaning" + 18: VacuumState.CLEANING, # "Segment cleaning" + 22: VacuumState.DOCKED, # "Emptying the bin" on s7+ + 23: VacuumState.DOCKED, # "Washing the mop" on s7maxV + 26: VacuumState.RETURNING, # "Going to wash the mop" on s7maxV + 100: VacuumState.DOCKED, # "Charging complete" + 101: VacuumState.ERROR, # "Device offline" } @@ -206,7 +206,7 @@ class MiroboVacuum( ) -> None: """Initialize the Xiaomi vacuum cleaner robot handler.""" super().__init__(device, entry, unique_id, coordinator) - self._state: VacuumEntityState | None = None + self._state: VacuumState | None = None async def async_added_to_hass(self) -> None: """Run when entity is about to be added to hass.""" @@ -214,12 +214,12 @@ class MiroboVacuum( self._handle_coordinator_update() @property - def vacuum_state(self) -> VacuumEntityState | None: + def vacuum_state(self) -> VacuumState | 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 VacuumEntityState.ERROR + return VacuumState.ERROR return self._state From 3cfef0cc86b0d7834cf81f923d672ce4e3f8b2a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Oct 2024 16:07:47 +0000 Subject: [PATCH 12/86] Tests --- tests/components/demo/test_vacuum.py | 32 ++++++------- .../components/google_assistant/test_trait.py | 10 ++-- .../components/homekit/test_type_switches.py | 6 +-- tests/components/litterrobot/test_init.py | 4 +- tests/components/litterrobot/test_vacuum.py | 14 +++--- tests/components/mqtt/test_vacuum.py | 12 ++--- tests/components/sharkiq/test_vacuum.py | 12 ++--- tests/components/template/test_vacuum.py | 46 +++++++++---------- tests/components/vacuum/__init__.py | 14 +++--- .../vacuum/test_device_condition.py | 10 ++-- .../components/vacuum/test_device_trigger.py | 16 +++---- tests/components/vacuum/test_init.py | 16 +++---- .../components/vacuum/test_reproduce_state.py | 32 ++++++------- tests/components/xiaomi_miio/test_vacuum.py | 6 +-- 14 files changed, 115 insertions(+), 115 deletions(-) diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index 64c6cac5f5c..8e2bf5acf6a 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -22,7 +22,7 @@ from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -71,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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.DOCKED async def test_methods(hass: HomeAssistant) -> None: @@ -107,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 == VacuumEntityState.CLEANING + assert state.state == VacuumState.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 == VacuumEntityState.IDLE + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.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 == VacuumEntityState.IDLE + assert state.state == VacuumState.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 == VacuumEntityState.RETURNING + assert state.state == VacuumState.RETURNING await common.async_set_fan_speed( hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_COMPLETE @@ -141,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 == VacuumEntityState.CLEANING + assert state.state == VacuumState.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 == VacuumEntityState.PAUSED + assert state.state == VacuumState.PAUSED await common.async_return_to_base(hass, ENTITY_VACUUM_COMPLETE) state = hass.states.get(ENTITY_VACUUM_COMPLETE) - assert state.state == VacuumEntityState.RETURNING + assert state.state == VacuumState.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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.DOCKED async def test_unsupported_methods(hass: HomeAssistant) -> None: @@ -247,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 == VacuumEntityState.IDLE + assert new_state_complete.state == VacuumState.IDLE diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 8052ec20c01..14108e6d16d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -432,7 +432,7 @@ async def test_dock_vacuum(hass: HomeAssistant) -> None: assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None, None) trt = trait.DockTrait( - hass, State("vacuum.bla", vacuum.VacuumEntityState.IDLE), BASIC_CONFIG + hass, State("vacuum.bla", vacuum.VacuumState.IDLE), BASIC_CONFIG ) assert trt.sync_attributes() == {} @@ -456,7 +456,7 @@ async def test_locate_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumEntityState.IDLE, + vacuum.VacuumState.IDLE, {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.LOCATE}, ), BASIC_CONFIG, @@ -487,7 +487,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumEntityState.DOCKED, + vacuum.VacuumState.DOCKED, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 100, @@ -513,7 +513,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumEntityState.CLEANING, + vacuum.VacuumState.CLEANING, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 20, @@ -553,7 +553,7 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumEntityState.PAUSED, + vacuum.VacuumState.PAUSED, {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE}, ), BASIC_CONFIG, diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 64358058234..1b1699f5282 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -27,7 +27,7 @@ from homeassistant.components.vacuum import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -294,7 +294,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, - VacuumEntityState.CLEANING, + VacuumState.CLEANING, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START @@ -305,7 +305,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, - VacuumEntityState.DOCKED, + VacuumState.DOCKED, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 6430f94b9d9..b23b5373579 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import litterrobot from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_START, - VacuumEntityState, + VacuumState, ) 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 == VacuumEntityState.DOCKED + assert vacuum.state == VacuumState.DOCKED await hass.services.async_call( VACUUM_DOMAIN, diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 89366261a38..4bfe50a3839 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -15,7 +15,7 @@ from homeassistant.components.vacuum import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_START, SERVICE_STOP, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -51,7 +51,7 @@ async def test_vacuum( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumEntityState.DOCKED + assert vacuum.state == VacuumState.DOCKED assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID) @@ -93,17 +93,17 @@ async def test_vacuum_with_error( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumEntityState.ERROR + assert vacuum.state == VacuumState.ERROR @pytest.mark.parametrize( ("robot_data", "expected_state"), [ - ({"displayCode": "DC_CAT_DETECT"}, VacuumEntityState.DOCKED), - ({"isDFIFull": True}, VacuumEntityState.ERROR), + ({"displayCode": "DC_CAT_DETECT"}, VacuumState.DOCKED), + ({"isDFIFull": True}, VacuumState.ERROR), ( {"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, - VacuumEntityState.PAUSED, + VacuumState.PAUSED, ), ], ) @@ -151,7 +151,7 @@ async def test_commands( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumEntityState.DOCKED + assert vacuum.state == VacuumState.DOCKED extra = extra or {} data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})} diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 907ef8b5c84..f90f323d199 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -27,7 +27,7 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, SERVICE_STOP, - VacuumEntityState, + VacuumState, ) from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -312,7 +312,7 @@ async def test_status( }""" async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") - assert state.state == VacuumEntityState.CLEANING + assert state.state == VacuumState.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" @@ -325,7 +325,7 @@ async def test_status( async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") - assert state.state == VacuumEntityState.DOCKED + assert state.state == VacuumState.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" @@ -365,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 == VacuumEntityState.CLEANING + assert state.state == VacuumState.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 @@ -379,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 == VacuumEntityState.CLEANING + assert state.state == VacuumState.CLEANING assert state.attributes.get(ATTR_FAN_SPEED) is None assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None @@ -393,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 == VacuumEntityState.DOCKED + assert state.state == VacuumState.DOCKED assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60" assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index ed3d9715521..2437f8372b7 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -36,7 +36,7 @@ from homeassistant.components.vacuum import ( SERVICE_START, SERVICE_STOP, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -157,7 +157,7 @@ async def test_simple_properties( assert entity assert state - assert state.state == VacuumEntityState.CLEANING + assert state.state == VacuumState.CLEANING assert entity.unique_id == "AC000Wxxxxxxxxx" @@ -186,10 +186,10 @@ async def test_initial_attributes( @pytest.mark.parametrize( ("service", "target_state"), [ - (SERVICE_STOP, VacuumEntityState.IDLE), - (SERVICE_PAUSE, VacuumEntityState.PAUSED), - (SERVICE_RETURN_TO_BASE, VacuumEntityState.RETURNING), - (SERVICE_START, VacuumEntityState.CLEANING), + (SERVICE_STOP, VacuumState.IDLE), + (SERVICE_PAUSE, VacuumState.PAUSED), + (SERVICE_RETURN_TO_BASE, VacuumState.RETURNING), + (SERVICE_START, VacuumState.CLEANING), ], ) async def test_cleaning_states( diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index ab7991fa948..da44c1c8e2d 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import pytest from homeassistant import setup -from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumEntityState +from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumState from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError @@ -37,7 +37,7 @@ _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" }, ), ( - VacuumEntityState.CLEANING, + VacuumState.CLEANING, 100, { "vacuum": { @@ -142,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, VacuumEntityState.CLEANING) + hass.states.async_set(_STATE_INPUT_SELECT, VacuumState.CLEANING) hass.states.async_set(_BATTERY_LEVEL_INPUT_NUMBER, 100) await hass.async_block_till_done() - _verify(hass, VacuumEntityState.CLEANING, 100) + _verify(hass, VacuumState.CLEANING, 100) @pytest.mark.parametrize( @@ -363,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 == VacuumEntityState.CLEANING - _verify(hass, VacuumEntityState.CLEANING, None) + assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumState.CLEANING + _verify(hass, VacuumState.CLEANING, None) assert len(calls) == 1 assert calls[-1].data["action"] == "start" assert calls[-1].data["caller"] == _TEST_VACUUM @@ -374,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 == VacuumEntityState.PAUSED - _verify(hass, VacuumEntityState.PAUSED, None) + assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumState.PAUSED + _verify(hass, VacuumState.PAUSED, None) assert len(calls) == 2 assert calls[-1].data["action"] == "pause" assert calls[-1].data["caller"] == _TEST_VACUUM @@ -385,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 == VacuumEntityState.IDLE - _verify(hass, VacuumEntityState.IDLE, None) + assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumState.IDLE + _verify(hass, VacuumState.IDLE, None) assert len(calls) == 3 assert calls[-1].data["action"] == "stop" assert calls[-1].data["caller"] == _TEST_VACUUM @@ -396,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 == VacuumEntityState.RETURNING - _verify(hass, VacuumEntityState.RETURNING, None) + assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumState.RETURNING + _verify(hass, VacuumState.RETURNING, None) assert len(calls) == 4 assert calls[-1].data["action"] == "return_to_base" assert calls[-1].data["caller"] == _TEST_VACUUM @@ -501,7 +501,7 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None: "input_select", { "input_select": { - "state": {"name": "State", "options": [VacuumEntityState.CLEANING]} + "state": {"name": "State", "options": [VacuumState.CLEANING]} } }, ) @@ -519,7 +519,7 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumEntityState.CLEANING, + "option": VacuumState.CLEANING, }, } } @@ -551,11 +551,11 @@ async def _register_components(hass: HomeAssistant) -> None: "state": { "name": "State", "options": [ - VacuumEntityState.CLEANING, - VacuumEntityState.DOCKED, - VacuumEntityState.IDLE, - VacuumEntityState.PAUSED, - VacuumEntityState.RETURNING, + VacuumState.CLEANING, + VacuumState.DOCKED, + VacuumState.IDLE, + VacuumState.PAUSED, + VacuumState.RETURNING, ], }, "fan_speed": { @@ -575,7 +575,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumEntityState.CLEANING, + "option": VacuumState.CLEANING, }, }, { @@ -591,7 +591,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumEntityState.PAUSED, + "option": VacuumState.PAUSED, }, }, { @@ -607,7 +607,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumEntityState.IDLE, + "option": VacuumState.IDLE, }, }, { @@ -623,7 +623,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumEntityState.RETURNING, + "option": VacuumState.RETURNING, }, }, { diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index daab7b81420..ab41e8bed7f 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -6,7 +6,7 @@ from homeassistant.components.vacuum import ( DOMAIN, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -35,20 +35,20 @@ class MockVacuum(MockEntity, StateVacuumEntity): def __init__(self, **values: Any) -> None: """Initialize a mock vacuum entity.""" super().__init__(**values) - self._attr_vacuum_state = VacuumEntityState.DOCKED + self._attr_vacuum_state = VacuumState.DOCKED self._attr_fan_speed = "slow" def stop(self, **kwargs: Any) -> None: """Stop cleaning.""" - self._attr_vacuum_state = VacuumEntityState.IDLE + self._attr_vacuum_state = VacuumState.IDLE def return_to_base(self, **kwargs: Any) -> None: """Return to base.""" - self._attr_vacuum_state = VacuumEntityState.RETURNING + self._attr_vacuum_state = VacuumState.RETURNING def clean_spot(self, **kwargs: Any) -> None: """Clean a spot.""" - self._attr_vacuum_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumState.CLEANING def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the fan speed.""" @@ -56,11 +56,11 @@ class MockVacuum(MockEntity, StateVacuumEntity): def start(self) -> None: """Start cleaning.""" - self._attr_vacuum_state = VacuumEntityState.CLEANING + self._attr_vacuum_state = VacuumState.CLEANING def pause(self) -> None: """Pause cleaning.""" - self._attr_vacuum_state = VacuumEntityState.PAUSED + self._attr_vacuum_state = VacuumState.PAUSED async def help_async_setup_entry_init( diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 6d083bb47ba..d0573c27ffc 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -5,7 +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, VacuumEntityState +from homeassistant.components.vacuum import DOMAIN, VacuumState from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -117,7 +117,7 @@ async def test_if_state( DOMAIN, "test", "5678", device_id=device_entry.id ) - hass.states.async_set(entry.entity_id, VacuumEntityState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumState.DOCKED) assert await async_setup_component( hass, @@ -169,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, VacuumEntityState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumState.CLEANING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -177,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, VacuumEntityState.RETURNING) + hass.states.async_set(entry.entity_id, VacuumState.RETURNING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -202,7 +202,7 @@ async def test_if_state_legacy( DOMAIN, "test", "5678", device_id=device_entry.id ) - hass.states.async_set(entry.entity_id, VacuumEntityState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumState.CLEANING) assert await async_setup_component( hass, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 31152e749e3..4fb5e281ccf 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -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, VacuumEntityState +from homeassistant.components.vacuum import DOMAIN, VacuumState 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, VacuumEntityState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumState.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, VacuumEntityState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumState.CLEANING) await hass.async_block_till_done() assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 7c5c45022e2..286ca729ab2 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -22,7 +22,7 @@ from homeassistant.components.vacuum import ( SERVICE_STOP, StateVacuumEntity, VacuumEntityFeature, - VacuumEntityState, + VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -77,7 +77,7 @@ def test_deprecated_constants( @pytest.mark.parametrize( - ("enum", "constant_prefix"), _create_tuples(vacuum.VacuumEntityState, "STATE_") + ("enum", "constant_prefix"), _create_tuples(vacuum.VacuumState, "STATE_") ) @pytest.mark.parametrize( "module", @@ -98,11 +98,11 @@ def test_deprecated_constants_for_state( @pytest.mark.parametrize( ("service", "expected_state"), [ - (SERVICE_CLEAN_SPOT, VacuumEntityState.CLEANING), - (SERVICE_PAUSE, VacuumEntityState.PAUSED), - (SERVICE_RETURN_TO_BASE, VacuumEntityState.RETURNING), - (SERVICE_START, VacuumEntityState.CLEANING), - (SERVICE_STOP, VacuumEntityState.IDLE), + (SERVICE_CLEAN_SPOT, VacuumState.CLEANING), + (SERVICE_PAUSE, VacuumState.PAUSED), + (SERVICE_RETURN_TO_BASE, VacuumState.RETURNING), + (SERVICE_START, VacuumState.CLEANING), + (SERVICE_STOP, VacuumState.IDLE), ], ) async def test_state_services( @@ -417,7 +417,7 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( def start(self) -> None: """Start cleaning.""" - self._attr_state = VacuumEntityState.CLEANING + self._attr_state = VacuumState.CLEANING entity = MockLegacyVacuum() diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index c83e86336f1..98d6afd3ea8 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -9,7 +9,7 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumEntityState, + VacuumState, ) from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, State @@ -30,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", VacuumEntityState.CLEANING, {}) - hass.states.async_set("vacuum.entity_docked", VacuumEntityState.DOCKED, {}) - hass.states.async_set("vacuum.entity_idle", VacuumEntityState.IDLE, {}) - hass.states.async_set("vacuum.entity_returning", VacuumEntityState.RETURNING, {}) - hass.states.async_set("vacuum.entity_paused", VacuumEntityState.PAUSED, {}) + hass.states.async_set("vacuum.entity_cleaning", VacuumState.CLEANING, {}) + hass.states.async_set("vacuum.entity_docked", VacuumState.DOCKED, {}) + hass.states.async_set("vacuum.entity_idle", VacuumState.IDLE, {}) + hass.states.async_set("vacuum.entity_returning", VacuumState.RETURNING, {}) + hass.states.async_set("vacuum.entity_paused", VacuumState.PAUSED, {}) turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON) turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF) @@ -51,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", VacuumEntityState.CLEANING), - State("vacuum.entity_docked", VacuumEntityState.DOCKED), - State("vacuum.entity_idle", VacuumEntityState.IDLE), - State("vacuum.entity_returning", VacuumEntityState.RETURNING), - State("vacuum.entity_paused", VacuumEntityState.PAUSED), + State("vacuum.entity_cleaning", VacuumState.CLEANING), + State("vacuum.entity_docked", VacuumState.DOCKED), + State("vacuum.entity_idle", VacuumState.IDLE), + State("vacuum.entity_returning", VacuumState.RETURNING), + State("vacuum.entity_paused", VacuumState.PAUSED), ], ) @@ -86,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", VacuumEntityState.PAUSED), - State("vacuum.entity_docked", VacuumEntityState.CLEANING), - State("vacuum.entity_idle", VacuumEntityState.DOCKED), - State("vacuum.entity_returning", VacuumEntityState.CLEANING), - State("vacuum.entity_paused", VacuumEntityState.IDLE), + State("vacuum.entity_cleaning", VacuumState.PAUSED), + State("vacuum.entity_docked", VacuumState.CLEANING), + State("vacuum.entity_idle", VacuumState.DOCKED), + State("vacuum.entity_returning", VacuumState.CLEANING), + State("vacuum.entity_paused", VacuumState.IDLE), # Should not raise State("vacuum.non_existing", STATE_ON), ], diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index e2e485e785c..8e9ef3976eb 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -21,7 +21,7 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumEntityState, + VacuumState, ) from homeassistant.components.xiaomi_miio.const import ( CONF_FLOW_TYPE, @@ -263,7 +263,7 @@ async def test_xiaomi_vacuum_services( # Check state attributes state = hass.states.get(entity_id) - assert state.state == VacuumEntityState.ERROR + assert state.state == VacuumState.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" @@ -449,7 +449,7 @@ async def test_xiaomi_specific_services( # Check state attributes state = hass.states.get(entity_id) - assert state.state == VacuumEntityState.CLEANING + assert state.state == VacuumState.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" From 4caaaf86bcb3ae1138a8ce33298d9f0f59192e6c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Oct 2024 16:17:27 +0000 Subject: [PATCH 13/86] Fixes --- homeassistant/components/vacuum/__init__.py | 8 ++++++++ tests/components/vacuum/conftest.py | 7 +++---- tests/components/vacuum/test_init.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 0d2af17f425..1b4ff98bf80 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -249,6 +249,14 @@ class StateVacuumEntity( __vacuum_legacy_state: bool = False __vacuum_legacy_state_reported: 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 'vacuum_state' property instead of + # setting the state directly. + cls.__vacuum_legacy_state = True + def __setattr__(self, __name: str, __value: Any) -> None: """Set attribute. diff --git a/tests/components/vacuum/conftest.py b/tests/components/vacuum/conftest.py index d6c197cc56a..7420614b5a3 100644 --- a/tests/components/vacuum/conftest.py +++ b/tests/components/vacuum/conftest.py @@ -39,8 +39,8 @@ def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: @pytest.fixture(name="supported_features") -async def alarm_control_panel_supported_features() -> VacuumEntityFeature: - """Return the supported features for the test alarm control panel entity.""" +async def vacuum_supported_features() -> VacuumEntityFeature: + """Return the supported features for the test vacuum entity.""" return ( VacuumEntityFeature.PAUSE | VacuumEntityFeature.STOP @@ -61,7 +61,7 @@ async def setup_vacuum_platform_test_entity( entity_registry: er.EntityRegistry, supported_features: VacuumEntityFeature, ) -> MagicMock: - """Set up alarm control panel entity using an entity platform.""" + """Set up vacuum entity using an entity platform.""" async def async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry @@ -80,7 +80,6 @@ async def setup_vacuum_platform_test_entity( ), ) - # Unnamed sensor without device class -> no name entity = MockVacuum( supported_features=supported_features, ) diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 286ca729ab2..e52832f58dd 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -354,7 +354,7 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( @property def state(self) -> str: """Return the state of the entity.""" - return "disarmed" + return VacuumState.CLEANING entity = MockLegacyVacuum() From 9a9673aaa47b86a36495f52843281d508472540f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:26:39 +0000 Subject: [PATCH 14/86] Fix VacuumEntity --- homeassistant/components/vacuum/__init__.py | 56 ++++++++----------- homeassistant/components/vacuum/const.py | 16 +++--- .../components/vacuum/device_condition.py | 6 +- .../components/vacuum/device_trigger.py | 6 +- .../components/vacuum/reproduce_state.py | 20 +++---- 5 files changed, 49 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 1b4ff98bf80..10ee0c4bf48 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -32,6 +32,7 @@ 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 report from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -44,7 +45,7 @@ from .const import ( # noqa: F401 _DEPRECATED_STATE_RETURNING, DOMAIN, STATES, - VacuumState, + VacuumActivity, ) _LOGGER = logging.getLogger(__name__) @@ -76,8 +77,8 @@ DEFAULT_NAME = "Vacuum cleaner robot" # These STATE_* constants are deprecated as of Home Assistant 2024.11. # Please use the VacuumState enum instead. -_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumState.IDLE, "2025.11") -_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumState.PAUSED, "2025.11") +_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2025.11") +_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2025.11") class VacuumEntityFeature(IntFlag): @@ -243,17 +244,16 @@ class StateVacuumEntity( _attr_battery_level: int | None = None _attr_fan_speed: str | None = None _attr_fan_speed_list: list[str] - _attr_vacuum_state: VacuumState | None = None + _attr_activity: VacuumActivity | None = None _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) __vacuum_legacy_state: bool = False - __vacuum_legacy_state_reported: 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 'vacuum_state' property instead of + # Integrations should use the 'activity' property instead of # setting the state directly. cls.__vacuum_legacy_state = True @@ -264,9 +264,7 @@ class StateVacuumEntity( unless already reported. """ if __name == "_attr_state": - if self.__vacuum_legacy_state_reported is not True: - self._report_deprecated_vacuum_state_handling() - self.__vacuum_legacy_state_reported = True + self._report_deprecated_vacuum_state_handling() return super().__setattr__(__name, __value) @callback @@ -278,28 +276,22 @@ class StateVacuumEntity( ) -> None: """Start adding an entity to a platform.""" super().add_to_platform_start(hass, platform, parallel_updates) - if self.__vacuum_legacy_state and not self.__vacuum_legacy_state_reported: + if self.__vacuum_legacy_state: self._report_deprecated_vacuum_state_handling() @callback def _report_deprecated_vacuum_state_handling(self) -> None: """Report on deprecated handling of vacuum state. - Integrations should implement vacuum_state instead of using state directly. + Integrations should implement activity instead of using state directly. """ - self.__vacuum_legacy_state_reported = True - if "custom_components" in type(self).__module__: - # Do not report on core integrations as they have been fixed. - report_issue = "report it to the custom integration author." - _LOGGER.warning( - "Entity %s (%s) is setting state directly" - " which will stop working in HA Core 2025.11." - " Entities should implement the 'vacuum_state' property and" - " return its state using the VacuumState enum, please %s", - self.entity_id, - type(self), - report_issue, - ) + report( + "is setting state directly which will stop working in HA Core 2025.11." + f" Entity {self.entity_id} ({type(self)}) should implement the 'activity' property and" + " return its state using the VacuumActivity enum, please ", + error_if_core=True, + error_if_integration=False, + ) @cached_property def battery_level(self) -> int | None: @@ -309,7 +301,7 @@ class StateVacuumEntity( @property def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner.""" - charging = bool(self.vacuum_state == VacuumState.DOCKED) + charging = bool(self.activity == VacuumActivity.DOCKED) return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -351,18 +343,18 @@ class StateVacuumEntity( @property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" - if (vacuum_state := self.vacuum_state) is None: + if (activity := self.activity) is None: return None - return vacuum_state + return activity @cached_property - def vacuum_state(self) -> VacuumState | None: - """Return the current vacuum state. + def activity(self) -> VacuumActivity | None: + """Return the current vacuum activity. - Integrations should overwrite this or use the '_attr_vacuum_state' - attribute to set the vacuum status using the 'VacuumState' enum. + Integrations should overwrite this or use the '_attr_activity' + attribute to set the vacuum activity using the 'VacuumActivity' enum. """ - return self._attr_vacuum_state + return self._attr_activity @cached_property def supported_features(self) -> VacuumEntityFeature: diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py index 05b631b5afe..1eb14144140 100644 --- a/homeassistant/components/vacuum/const.py +++ b/homeassistant/components/vacuum/const.py @@ -15,8 +15,8 @@ from homeassistant.helpers.deprecation import ( DOMAIN = "vacuum" -class VacuumState(StrEnum): - """Vacuum entity states.""" +class VacuumActivity(StrEnum): + """Vacuum activity states.""" CLEANING = "cleaning" DOCKED = "docked" @@ -28,13 +28,15 @@ class VacuumState(StrEnum): # These STATE_* constants are deprecated as of Home Assistant 2024.11. # Please use the VacuumState enum instead. -_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumState.CLEANING, "2025.11") -_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumState.DOCKED, "2025.11") -_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum(VacuumState.RETURNING, "2025.11") -_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumState.ERROR, "2025.11") +_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumActivity.CLEANING, "2025.11") +_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumActivity.DOCKED, "2025.11") +_DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum( + VacuumActivity.RETURNING, "2025.11" +) +_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumActivity.ERROR, "2025.11") -STATES = [cls.value for cls in VacuumState] +STATES = [cls.value for cls in VacuumActivity] # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 072136b5a4a..4da64484bf7 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -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, VacuumState +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 = [VacuumState.DOCKED] + test_states = [VacuumActivity.DOCKED] else: - test_states = [VacuumState.CLEANING, VacuumState.RETURNING] + test_states = [VacuumActivity.CLEANING, VacuumActivity.RETURNING] registry = er.async_get(hass) entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID]) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index ec4238674af..fe682ef21d3 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -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, VacuumState +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 = VacuumState.CLEANING + to_state = VacuumActivity.CLEANING else: - to_state = VacuumState.DOCKED + to_state = VacuumActivity.DOCKED state_config = { CONF_PLATFORM: "state", diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index dde9d6ca980..ef3fb329686 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -24,18 +24,18 @@ from . import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumState, + VacuumActivity, ) _LOGGER = logging.getLogger(__name__) VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} VALID_STATES_STATE = { - VacuumState.CLEANING, - VacuumState.DOCKED, - VacuumState.IDLE, - VacuumState.PAUSED, - VacuumState.RETURNING, + VacuumActivity.CLEANING, + VacuumActivity.DOCKED, + VacuumActivity.IDLE, + VacuumActivity.PAUSED, + VacuumActivity.RETURNING, } @@ -71,13 +71,13 @@ async def _async_reproduce_state( service = SERVICE_TURN_ON elif state.state == STATE_OFF: service = SERVICE_TURN_OFF - elif state.state == VacuumState.CLEANING: + elif state.state == VacuumActivity.CLEANING: service = SERVICE_START - elif state.state in [VacuumState.DOCKED, VacuumState.RETURNING]: + elif state.state in [VacuumActivity.DOCKED, VacuumActivity.RETURNING]: service = SERVICE_RETURN_TO_BASE - elif state.state == VacuumState.IDLE: + elif state.state == VacuumActivity.IDLE: service = SERVICE_STOP - elif state.state == VacuumState.PAUSED: + elif state.state == VacuumActivity.PAUSED: service = SERVICE_PAUSE await hass.services.async_call( From e4835a31c72c17181814c6eb9d38f400805c8c25 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:38:57 +0000 Subject: [PATCH 15/86] Mods --- tests/components/vacuum/__init__.py | 14 +++---- .../vacuum/test_device_condition.py | 10 ++--- .../components/vacuum/test_device_trigger.py | 16 ++++---- tests/components/vacuum/test_init.py | 40 ++++++++----------- .../components/vacuum/test_reproduce_state.py | 32 +++++++-------- 5 files changed, 53 insertions(+), 59 deletions(-) diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index ab41e8bed7f..26e31a87eee 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -5,8 +5,8 @@ from typing import Any from homeassistant.components.vacuum import ( DOMAIN, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -35,20 +35,20 @@ class MockVacuum(MockEntity, StateVacuumEntity): def __init__(self, **values: Any) -> None: """Initialize a mock vacuum entity.""" super().__init__(**values) - self._attr_vacuum_state = VacuumState.DOCKED + self._attr_activity = VacuumActivity.DOCKED self._attr_fan_speed = "slow" def stop(self, **kwargs: Any) -> None: """Stop cleaning.""" - self._attr_vacuum_state = VacuumState.IDLE + self._attr_activity = VacuumActivity.IDLE def return_to_base(self, **kwargs: Any) -> None: """Return to base.""" - self._attr_vacuum_state = VacuumState.RETURNING + self._attr_activity = VacuumActivity.RETURNING def clean_spot(self, **kwargs: Any) -> None: """Clean a spot.""" - self._attr_vacuum_state = VacuumState.CLEANING + self._attr_activity = VacuumActivity.CLEANING def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the fan speed.""" @@ -56,11 +56,11 @@ class MockVacuum(MockEntity, StateVacuumEntity): def start(self) -> None: """Start cleaning.""" - self._attr_vacuum_state = VacuumState.CLEANING + self._attr_activity = VacuumActivity.CLEANING def pause(self) -> None: """Pause cleaning.""" - self._attr_vacuum_state = VacuumState.PAUSED + self._attr_activity = VacuumActivity.PAUSED async def help_async_setup_entry_init( diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index d0573c27ffc..5a1b1fea7de 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -5,7 +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, VacuumState +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 @@ -117,7 +117,7 @@ async def test_if_state( DOMAIN, "test", "5678", device_id=device_entry.id ) - hass.states.async_set(entry.entity_id, VacuumState.DOCKED) + hass.states.async_set(entry.entity_id, VacuumActivity.DOCKED) assert await async_setup_component( hass, @@ -169,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, VacuumState.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() @@ -177,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, VacuumState.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() @@ -202,7 +202,7 @@ async def test_if_state_legacy( DOMAIN, "test", "5678", device_id=device_entry.id ) - hass.states.async_set(entry.entity_id, VacuumState.CLEANING) + hass.states.async_set(entry.entity_id, VacuumActivity.CLEANING) assert await async_setup_component( hass, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 4fb5e281ccf..3a0cbafb4a1 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -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, VacuumState +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, VacuumState.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, VacuumState.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, VacuumState.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, VacuumState.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, VacuumState.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, VacuumState.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, VacuumState.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)) diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index e52832f58dd..e1f240b37cb 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -21,8 +21,8 @@ from homeassistant.components.vacuum import ( SERVICE_START, SERVICE_STOP, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -77,7 +77,7 @@ def test_deprecated_constants( @pytest.mark.parametrize( - ("enum", "constant_prefix"), _create_tuples(vacuum.VacuumState, "STATE_") + ("enum", "constant_prefix"), _create_tuples(vacuum.VacuumActivity, "STATE_") ) @pytest.mark.parametrize( "module", @@ -98,11 +98,11 @@ def test_deprecated_constants_for_state( @pytest.mark.parametrize( ("service", "expected_state"), [ - (SERVICE_CLEAN_SPOT, VacuumState.CLEANING), - (SERVICE_PAUSE, VacuumState.PAUSED), - (SERVICE_RETURN_TO_BASE, VacuumState.RETURNING), - (SERVICE_START, VacuumState.CLEANING), - (SERVICE_STOP, VacuumState.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( @@ -135,9 +135,9 @@ async def test_state_services( {"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: @@ -316,12 +316,10 @@ async def test_vacuum_not_log_deprecated_state_warning( mock_vacuum_entity: MockVacuum, caplog: pytest.LogCaptureFixture, ) -> None: - """Test correctly using vacuum_state doesn't log issue or raise repair.""" + """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 ( - "Entities should implement the 'vacuum_state' property and" not in caplog.text - ) + assert "Entities should implement the 'activity' property and" not in caplog.text async def test_vacuum_log_deprecated_state_warning_using_state_prop( @@ -354,7 +352,7 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( @property def state(self) -> str: """Return the state of the entity.""" - return VacuumState.CLEANING + return VacuumActivity.CLEANING entity = MockLegacyVacuum() @@ -385,7 +383,7 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( state = hass.states.get(entity.entity_id) assert state is not None - assert "Entities should implement the 'vacuum_state' property and" in caplog.text + assert "Entities should implement the 'activity' property and" in caplog.text async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( @@ -417,7 +415,7 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( def start(self) -> None: """Start cleaning.""" - self._attr_state = VacuumState.CLEANING + self._attr_state = VacuumActivity.CLEANING entity = MockLegacyVacuum() @@ -448,9 +446,7 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( state = hass.states.get(entity.entity_id) assert state is not None - assert ( - "Entities should implement the 'vacuum_state' property and" not in caplog.text - ) + assert "Entities should implement the 'activity' property and" not in caplog.text with patch.object( MockLegacyVacuum, @@ -459,7 +455,7 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( ): await async_start(hass, entity.entity_id) - assert "Entities should implement the 'vacuum_state' property and" in caplog.text + assert "Entities should implement the 'activity' property and" in caplog.text caplog.clear() with patch.object( MockLegacyVacuum, @@ -468,6 +464,4 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( ): await async_start(hass, entity.entity_id) # Test we only log once - assert ( - "Entities should implement the 'vacuum_state' property and" not in caplog.text - ) + assert "Entities should implement the 'activity' property and" not in caplog.text diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index 98d6afd3ea8..dc5d81e8f08 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -9,7 +9,7 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumState, + VacuumActivity, ) from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, State @@ -30,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", VacuumState.CLEANING, {}) - hass.states.async_set("vacuum.entity_docked", VacuumState.DOCKED, {}) - hass.states.async_set("vacuum.entity_idle", VacuumState.IDLE, {}) - hass.states.async_set("vacuum.entity_returning", VacuumState.RETURNING, {}) - hass.states.async_set("vacuum.entity_paused", VacuumState.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) @@ -51,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", VacuumState.CLEANING), - State("vacuum.entity_docked", VacuumState.DOCKED), - State("vacuum.entity_idle", VacuumState.IDLE), - State("vacuum.entity_returning", VacuumState.RETURNING), - State("vacuum.entity_paused", VacuumState.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), ], ) @@ -86,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", VacuumState.PAUSED), - State("vacuum.entity_docked", VacuumState.CLEANING), - State("vacuum.entity_idle", VacuumState.DOCKED), - State("vacuum.entity_returning", VacuumState.CLEANING), - State("vacuum.entity_paused", VacuumState.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), ], From 02f496f465b95c26a3434ea09c8b06719b8dd981 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:39:15 +0000 Subject: [PATCH 16/86] Mods --- .../components/alexa/capabilities.py | 2 +- homeassistant/components/demo/vacuum.py | 28 +++++------ homeassistant/components/ecovacs/vacuum.py | 30 +++++------ .../components/google_assistant/trait.py | 10 ++-- homeassistant/components/group/registry.py | 8 +-- .../components/homekit/type_switches.py | 4 +- .../components/litterrobot/vacuum.py | 26 +++++----- homeassistant/components/mqtt/vacuum.py | 20 ++++---- homeassistant/components/neato/vacuum.py | 20 ++++---- homeassistant/components/roborock/vacuum.py | 50 +++++++++---------- homeassistant/components/romy/vacuum.py | 6 +-- 11 files changed, 102 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 027d52be28c..9f15c9adc54 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -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.VacuumState.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: diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index d5552f79f33..0059f7c6a54 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -8,8 +8,8 @@ from typing import Any from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -87,13 +87,13 @@ class StateDemoVacuum(StateVacuumEntity): """Initialize the vacuum.""" self._attr_name = name self._attr_supported_features = supported_features - self._state = VacuumState.DOCKED + self._state = VacuumActivity.DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 @property - def vacuum_state(self) -> VacuumState: + def activity(self) -> VacuumActivity: """Return the current state of the vacuum.""" return self._state @@ -119,33 +119,33 @@ class StateDemoVacuum(StateVacuumEntity): def start(self) -> None: """Start or resume the cleaning task.""" - if self._state != VacuumState.CLEANING: - self._state = VacuumState.CLEANING + if self._state != VacuumActivity.CLEANING: + self._state = 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 == VacuumState.CLEANING: - self._state = VacuumState.PAUSED + if self._state == VacuumActivity.CLEANING: + self._state = VacuumActivity.PAUSED self.schedule_update_ha_state() def stop(self, **kwargs: Any) -> None: """Stop the cleaning task, do not return to dock.""" - self._state = VacuumState.IDLE + self._state = VacuumActivity.IDLE self.schedule_update_ha_state() def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" - self._state = VacuumState.RETURNING + self._state = 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 = VacuumState.CLEANING + self._state = VacuumActivity.CLEANING self._cleaned_area += 1.32 self._battery_level -= 1 self.schedule_update_ha_state() @@ -163,12 +163,12 @@ class StateDemoVacuum(StateVacuumEntity): "persistent_notification", service_data={"message": "I'm here!", "title": "Locate request"}, ) - self._state = VacuumState.IDLE + self._state = VacuumActivity.IDLE self.async_write_ha_state() async def async_clean_spot(self, **kwargs: Any) -> None: """Locate the vacuum's position.""" - self._state = VacuumState.CLEANING + self._state = VacuumActivity.CLEANING self.async_write_ha_state() async def async_send_command( @@ -178,9 +178,9 @@ class StateDemoVacuum(StateVacuumEntity): **kwargs: Any, ) -> None: """Send a command to the vacuum.""" - self._state = VacuumState.IDLE + self._state = VacuumActivity.IDLE self.async_write_ha_state() def __set_state_to_dock(self, _: datetime) -> None: - self._state = VacuumState.DOCKED + self._state = VacuumActivity.DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 73efd7e4e3e..dde4fd64b56 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -15,8 +15,8 @@ import sucks from homeassistant.components.vacuum import ( StateVacuumEntity, StateVacuumEntityDescription, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError @@ -118,22 +118,22 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): self.schedule_update_ha_state() @property - def vacuum_state(self) -> VacuumState | None: + def activity(self) -> VacuumActivity | None: """Return the state of the vacuum cleaner.""" if self.error is not None: - return VacuumState.ERROR + return VacuumActivity.ERROR if self.device.is_cleaning: - return VacuumState.CLEANING + return VacuumActivity.CLEANING if self.device.is_charging: - return VacuumState.DOCKED + return VacuumActivity.DOCKED if self.device.vacuum_status == sucks.CLEAN_MODE_STOP: - return VacuumState.IDLE + return VacuumActivity.IDLE if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING: - return VacuumState.RETURNING + return VacuumActivity.RETURNING return None @@ -197,7 +197,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - if self.state == VacuumState.CLEANING: + if self.state == VacuumActivity.CLEANING: self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command( @@ -220,12 +220,12 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): _STATE_TO_VACUUM_STATE = { - State.IDLE: VacuumState.IDLE, - State.CLEANING: VacuumState.CLEANING, - State.RETURNING: VacuumState.RETURNING, - State.DOCKED: VacuumState.DOCKED, - State.ERROR: VacuumState.ERROR, - State.PAUSED: VacuumState.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" @@ -279,7 +279,7 @@ class EcovacsVacuum( self.async_write_ha_state() async def on_status(event: StateEvent) -> None: - self._attr_vacuum_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) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 899f0e3d8c7..8025a291031 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -729,7 +729,7 @@ class DockTrait(_Trait): def query_attributes(self) -> dict[str, Any]: """Return dock query attributes.""" - return {"isDocked": self.state.state == vacuum.VacuumState.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.VacuumState.DOCKED, - "isPluggedIn": self.state.state == vacuum.VacuumState.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.VacuumState.CLEANING, - "isPaused": state == vacuum.VacuumState.PAUSED, + "isRunning": state == vacuum.VacuumActivity.CLEANING, + "isPaused": state == vacuum.VacuumActivity.PAUSED, } if domain in COVER_VALVE_DOMAINS: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 38fe29fd253..2f3c4aa5221 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -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 VacuumState +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, - VacuumState.CLEANING, - VacuumState.RETURNING, - VacuumState.ERROR, + VacuumActivity.CLEANING, + VacuumActivity.RETURNING, + VacuumActivity.ERROR, }, STATE_ON, STATE_OFF, diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index fa2ac2cc404..0482a5956ac 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -21,8 +21,8 @@ from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -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 (VacuumState.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) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 0d1f32f4ac5..bd00c328233 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -12,8 +12,8 @@ import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, StateVacuumEntityDescription, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform @@ -26,16 +26,16 @@ from .entity import LitterRobotEntity SERVICE_SET_SLEEP_MODE = "set_sleep_mode" LITTER_BOX_STATUS_STATE_MAP = { - LitterBoxStatus.CLEAN_CYCLE: VacuumState.CLEANING, - LitterBoxStatus.EMPTY_CYCLE: VacuumState.CLEANING, - LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumState.DOCKED, - LitterBoxStatus.CAT_DETECTED: VacuumState.DOCKED, - LitterBoxStatus.CAT_SENSOR_TIMING: VacuumState.DOCKED, - LitterBoxStatus.DRAWER_FULL_1: VacuumState.DOCKED, - LitterBoxStatus.DRAWER_FULL_2: VacuumState.DOCKED, - LitterBoxStatus.READY: VacuumState.DOCKED, - LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumState.PAUSED, - LitterBoxStatus.OFF: VacuumState.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( @@ -75,9 +75,9 @@ class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity): ) @property - def vacuum_state(self) -> VacuumState: + def activity(self) -> VacuumActivity: """Return the state of the cleaner.""" - return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, VacuumState.ERROR) + return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, VacuumActivity.ERROR) @property def status(self) -> str: diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 9891e844f74..e1dbc061ebc 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -11,8 +11,8 @@ from homeassistant.components import vacuum from homeassistant.components.vacuum import ( ENTITY_ID_FORMAT, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME @@ -42,13 +42,13 @@ STATE_PAUSED = "paused" STATE_RETURNING = "returning" STATE_CLEANING = "cleaning" -POSSIBLE_STATES: dict[str, VacuumState] = { - STATE_IDLE: VacuumState.IDLE, - STATE_DOCKED: VacuumState.DOCKED, - STATE_ERROR: VacuumState.ERROR, - STATE_PAUSED: VacuumState.PAUSED, - STATE_RETURNING: VacuumState.RETURNING, - STATE_CLEANING: VacuumState.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 @@ -262,7 +262,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): if STATE in payload and ( (state := payload[STATE]) in POSSIBLE_STATES or state is None ): - self._attr_vacuum_state = ( + self._attr_activity = ( POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None ) del payload[STATE] @@ -274,7 +274,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self.add_subscription( CONF_STATE_TOPIC, self._state_message_received, - {"_attr_battery_level", "_attr_fan_speed", "_attr_vacuum_state"}, + {"_attr_battery_level", "_attr_fan_speed", "_attr_activity"}, ) async def _subscribe_topics(self) -> None: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index ae1b93346b9..1a9285964a2 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -13,8 +13,8 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_STATUS, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE @@ -166,23 +166,23 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): robot_alert = None if self._state["state"] == 1: if self._state["details"]["isCharging"]: - self._attr_vacuum_state = VacuumState.DOCKED + self._attr_activity = VacuumActivity.DOCKED self._status_state = "Charging" elif ( self._state["details"]["isDocked"] and not self._state["details"]["isCharging"] ): - self._attr_vacuum_state = VacuumState.DOCKED + self._attr_activity = VacuumActivity.DOCKED self._status_state = "Docked" else: - self._attr_vacuum_state = VacuumState.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_vacuum_state = VacuumState.CLEANING + self._attr_activity = VacuumActivity.CLEANING self._status_state = ( f"{MODE.get(self._state['cleaning']['mode'])} " f"{ACTION.get(self._state['action'])}" @@ -197,10 +197,10 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): else: self._status_state = robot_alert elif self._state["state"] == 3: - self._attr_vacuum_state = VacuumState.PAUSED + self._attr_activity = VacuumActivity.PAUSED self._status_state = "Paused" elif self._state["state"] == 4: - self._attr_vacuum_state = VacuumState.ERROR + self._attr_activity = VacuumActivity.ERROR self._status_state = ERRORS.get(self._state["error"]) self._attr_battery_level = self._state["details"]["charge"] @@ -323,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_vacuum_state == VacuumState.CLEANING: + if self._attr_activity == VacuumActivity.CLEANING: self.robot.pause_cleaning() - self._attr_vacuum_state = VacuumState.RETURNING + self._attr_activity = VacuumActivity.RETURNING self.robot.send_to_base() except NeatoRobotException as ex: _LOGGER.error( @@ -377,7 +377,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): "Start cleaning zone '%s' with robot %s", zone, self.entity_id ) - self._attr_vacuum_state = VacuumState.CLEANING + self._attr_activity = VacuumActivity.CLEANING try: self.robot.start_cleaning(mode, navigation, category, boundary_id) except NeatoRobotException as ex: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 457152ad01c..d3413bd7cbd 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -9,8 +9,8 @@ from roborock.roborock_typing import RoborockCommand from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse from homeassistant.helpers import entity_platform @@ -22,29 +22,29 @@ from .coordinator import RoborockDataUpdateCoordinator from .entity import RoborockCoordinatedEntityV1 STATE_CODE_TO_STATE = { - RoborockStateCode.starting: VacuumState.IDLE, # "Starting" - RoborockStateCode.charger_disconnected: VacuumState.IDLE, # "Charger disconnected" - RoborockStateCode.idle: VacuumState.IDLE, # "Idle" - RoborockStateCode.remote_control_active: VacuumState.CLEANING, # "Remote control active" - RoborockStateCode.cleaning: VacuumState.CLEANING, # "Cleaning" - RoborockStateCode.returning_home: VacuumState.RETURNING, # "Returning home" - RoborockStateCode.manual_mode: VacuumState.CLEANING, # "Manual mode" - RoborockStateCode.charging: VacuumState.DOCKED, # "Charging" - RoborockStateCode.charging_problem: VacuumState.ERROR, # "Charging problem" - RoborockStateCode.paused: VacuumState.PAUSED, # "Paused" - RoborockStateCode.spot_cleaning: VacuumState.CLEANING, # "Spot cleaning" - RoborockStateCode.error: VacuumState.ERROR, # "Error" - RoborockStateCode.shutting_down: VacuumState.IDLE, # "Shutting down" - RoborockStateCode.updating: VacuumState.DOCKED, # "Updating" - RoborockStateCode.docking: VacuumState.RETURNING, # "Docking" - RoborockStateCode.going_to_target: VacuumState.CLEANING, # "Going to target" - RoborockStateCode.zoned_cleaning: VacuumState.CLEANING, # "Zoned cleaning" - RoborockStateCode.segment_cleaning: VacuumState.CLEANING, # "Segment cleaning" - RoborockStateCode.emptying_the_bin: VacuumState.DOCKED, # "Emptying the bin" on s7+ - RoborockStateCode.washing_the_mop: VacuumState.DOCKED, # "Washing the mop" on s7maxV - RoborockStateCode.going_to_wash_the_mop: VacuumState.RETURNING, # "Going to wash the mop" on s7maxV - RoborockStateCode.charging_complete: VacuumState.DOCKED, # "Charging complete" - RoborockStateCode.device_offline: VacuumState.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" } @@ -107,7 +107,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): self._attr_fan_speed_list = self._device_status.fan_power_options @property - def vacuum_state(self) -> VacuumState | 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) diff --git a/homeassistant/components/romy/vacuum.py b/homeassistant/components/romy/vacuum.py index eff335cbd81..b6974d7edae 100644 --- a/homeassistant/components/romy/vacuum.py +++ b/homeassistant/components/romy/vacuum.py @@ -8,8 +8,8 @@ from typing import Any from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -81,9 +81,9 @@ class RomyVacuumEntity(RomyEntity, StateVacuumEntity): self._attr_battery_level = self.romy.battery_level try: assert self.romy.status is not None - self._attr_vacuum_state = VacuumState(self.romy.status) + self._attr_activity = VacuumActivity(self.romy.status) except (AssertionError, ValueError): - self._attr_vacuum_state = None + self._attr_activity = None self.async_write_ha_state() From 6b50da1db2724a055193e09412ee22cae7fc0a40 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:40:02 +0000 Subject: [PATCH 17/86] Mods --- homeassistant/components/roomba/entity.py | 42 +++++++-------- homeassistant/components/sharkiq/vacuum.py | 16 +++--- .../components/switchbot_cloud/vacuum.py | 28 +++++----- homeassistant/components/template/vacuum.py | 16 +++--- homeassistant/components/tuya/vacuum.py | 52 +++++++++--------- homeassistant/components/vacuum/__init__.py | 10 ++-- homeassistant/components/vacuum/const.py | 2 +- .../components/xiaomi_miio/vacuum.py | 54 +++++++++---------- tests/components/demo/test_vacuum.py | 32 +++++------ .../components/google_assistant/test_trait.py | 10 ++-- .../components/homekit/test_type_switches.py | 6 +-- tests/components/litterrobot/test_init.py | 4 +- tests/components/litterrobot/test_vacuum.py | 16 +++--- tests/components/mqtt/test_vacuum.py | 12 ++--- tests/components/sharkiq/test_vacuum.py | 12 ++--- tests/components/template/test_vacuum.py | 46 ++++++++-------- tests/components/xiaomi_miio/test_vacuum.py | 6 +-- 17 files changed, 181 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/roomba/entity.py b/homeassistant/components/roomba/entity.py index 47d1072c534..ae9486f2254 100644 --- a/homeassistant/components/roomba/entity.py +++ b/homeassistant/components/roomba/entity.py @@ -8,8 +8,8 @@ import logging from homeassistant.components.vacuum import ( ATTR_STATUS, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.const import ATTR_CONNECTIONS import homeassistant.helpers.device_registry as dr @@ -43,16 +43,16 @@ SUPPORT_IROBOT = ( ) STATE_MAP = { - "": VacuumState.IDLE, - "charge": VacuumState.DOCKED, - "evac": VacuumState.RETURNING, # Emptying at cleanbase - "hmMidMsn": VacuumState.CLEANING, # Recharging at the middle of a cycle - "hmPostMsn": VacuumState.RETURNING, # Cycle finished - "hmUsrDock": VacuumState.RETURNING, - "pause": VacuumState.PAUSED, - "run": VacuumState.CLEANING, - "stop": VacuumState.IDLE, - "stuck": VacuumState.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, } @@ -125,7 +125,7 @@ class IRobotEntity(Entity): return dt_util.utc_from_timestamp(ts) @property - def _robot_state(self) -> VacuumState: + def _robot_state(self) -> VacuumActivity: """Return the state of the vacuum cleaner.""" clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {}) cycle = clean_mission_status.get("cycle") @@ -133,12 +133,12 @@ class IRobotEntity(Entity): try: state = STATE_MAP[phase] except KeyError: - return VacuumState.ERROR + return VacuumActivity.ERROR if cycle != "none" and state in ( - VacuumState.IDLE, - VacuumState.DOCKED, + VacuumActivity.IDLE, + VacuumActivity.DOCKED, ): - state = VacuumState.PAUSED + state = VacuumActivity.PAUSED return state async def async_added_to_hass(self): @@ -169,7 +169,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1 @property - def vacuum_state(self) -> VacuumState: + def vacuum_state(self) -> VacuumActivity: """Return the state of the vacuum cleaner.""" return self._robot_state @@ -189,7 +189,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf # Only add cleaning time and cleaned area attrs when the vacuum is # currently on - if self.state == VacuumState.CLEANING: + if self.state == VacuumActivity.CLEANING: # Get clean mission status ( state_attrs[ATTR_CLEANING_TIME], @@ -243,7 +243,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_start(self): """Start or resume the cleaning task.""" - if self.vacuum_state == VacuumState.PAUSED: + if self.vacuum_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") @@ -258,10 +258,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self.vacuum_state == VacuumState.CLEANING: + if self.vacuum_state == VacuumActivity.CLEANING: await self.async_pause() for _ in range(10): - if self.state == VacuumState.PAUSED: + if self.state == VacuumActivity.PAUSED: break await asyncio.sleep(1) await self.hass.async_add_executor_job(self.vacuum.send_command, "dock") diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index db0f5bba9d6..9fa0a463465 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -10,8 +10,8 @@ import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -26,10 +26,10 @@ from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK from .coordinator import SharkIqUpdateCoordinator OPERATING_STATE_MAP = { - OperatingModes.PAUSE: VacuumState.PAUSED, - OperatingModes.START: VacuumState.CLEANING, - OperatingModes.STOP: VacuumState.IDLE, - OperatingModes.RETURN: VacuumState.RETURNING, + OperatingModes.PAUSE: VacuumActivity.PAUSED, + OperatingModes.START: VacuumActivity.CLEANING, + OperatingModes.STOP: VacuumActivity.IDLE, + OperatingModes.RETURN: VacuumActivity.RETURNING, } FAN_SPEEDS_MAP = { @@ -147,7 +147,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.error_text @property - def operating_mode(self) -> VacuumState | None: + def operating_mode(self) -> VacuumActivity | None: """Operating mode.""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @@ -158,7 +158,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @property - def vacuum_state(self) -> VacuumState | 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. @@ -166,7 +166,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum user a notification. """ if self.sharkiq.get_property_value(Properties.CHARGING_STATUS): - return VacuumState.DOCKED + return VacuumActivity.DOCKED return self.operating_mode @property diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index 94231a83fb5..2d2a1783d73 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -6,8 +6,8 @@ from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -38,17 +38,17 @@ async def async_setup_entry( ) -VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumState] = { - "StandBy": VacuumState.IDLE, - "Clearing": VacuumState.CLEANING, - "Paused": VacuumState.PAUSED, - "GotoChargeBase": VacuumState.RETURNING, - "Charging": VacuumState.DOCKED, - "ChargeDone": VacuumState.DOCKED, - "Dormant": VacuumState.IDLE, - "InTrouble": VacuumState.ERROR, - "InRemoteControl": VacuumState.CLEANING, - "InDustCollecting": VacuumState.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] = { @@ -109,9 +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_vacuum_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() diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 62d55c2375e..19029cc708b 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -18,8 +18,8 @@ from homeassistant.components.vacuum import ( SERVICE_START, SERVICE_STOP, StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -53,12 +53,12 @@ CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" _VALID_STATES = [ - VacuumState.CLEANING, - VacuumState.DOCKED, - VacuumState.PAUSED, - VacuumState.IDLE, - VacuumState.RETURNING, - VacuumState.ERROR, + VacuumActivity.CLEANING, + VacuumActivity.DOCKED, + VacuumActivity.PAUSED, + VacuumActivity.IDLE, + VacuumActivity.RETURNING, + VacuumActivity.ERROR, ] VACUUM_SCHEMA = vol.All( @@ -197,7 +197,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] @property - def vacuum_state(self) -> VacuumState | None: + def activity(self) -> VacuumActivity | None: """Return the status of the vacuum cleaner.""" return self._state diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 0a9429f98ae..738492102a1 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -8,8 +8,8 @@ from tuya_sharing import CustomerDevice, Manager from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,29 +21,29 @@ from .entity import EnumTypeData, IntegerTypeData, TuyaEntity TUYA_MODE_RETURN_HOME = "chargego" TUYA_STATUS_TO_HA = { - "charge_done": VacuumState.DOCKED, - "chargecompleted": VacuumState.DOCKED, - "chargego": VacuumState.DOCKED, - "charging": VacuumState.DOCKED, - "cleaning": VacuumState.CLEANING, - "docking": VacuumState.RETURNING, - "goto_charge": VacuumState.RETURNING, - "goto_pos": VacuumState.CLEANING, - "mop_clean": VacuumState.CLEANING, - "part_clean": VacuumState.CLEANING, - "paused": VacuumState.PAUSED, - "pick_zone_clean": VacuumState.CLEANING, - "pos_arrived": VacuumState.CLEANING, - "pos_unarrive": VacuumState.CLEANING, - "random": VacuumState.CLEANING, - "sleep": VacuumState.IDLE, - "smart_clean": VacuumState.CLEANING, - "smart": VacuumState.CLEANING, - "spot_clean": VacuumState.CLEANING, - "standby": VacuumState.IDLE, - "wall_clean": VacuumState.CLEANING, - "wall_follow": VacuumState.CLEANING, - "zone_clean": VacuumState.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, } @@ -134,12 +134,12 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): return self.device.status.get(DPCode.SUCTION) @property - def vacuum_state(self) -> VacuumState | 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 VacuumState.PAUSED + return VacuumActivity.PAUSED if not (status := self.device.status.get(DPCode.STATUS)): return None return TUYA_STATUS_TO_HA.get(status) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 10ee0c4bf48..f03ff4833fa 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -76,7 +76,7 @@ SERVICE_STOP = "stop" DEFAULT_NAME = "Vacuum cleaner robot" # These STATE_* constants are deprecated as of Home Assistant 2024.11. -# Please use the VacuumState enum instead. +# Please use the VacuumActivity enum instead. _DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2025.11") _DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2025.11") @@ -227,7 +227,7 @@ STATE_VACUUM_CACHED_PROPERTIES_WITH_ATTR_ = { "battery_icon", "fan_speed", "fan_speed_list", - "vacuum_state", + "activity", } @@ -264,7 +264,7 @@ class StateVacuumEntity( unless already reported. """ if __name == "_attr_state": - self._report_deprecated_vacuum_state_handling() + self._report_deprecated_activity_handling() return super().__setattr__(__name, __value) @callback @@ -277,10 +277,10 @@ class StateVacuumEntity( """Start adding an entity to a platform.""" super().add_to_platform_start(hass, platform, parallel_updates) if self.__vacuum_legacy_state: - self._report_deprecated_vacuum_state_handling() + self._report_deprecated_activity_handling() @callback - def _report_deprecated_vacuum_state_handling(self) -> None: + def _report_deprecated_activity_handling(self) -> None: """Report on deprecated handling of vacuum state. Integrations should implement activity instead of using state directly. diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py index 1eb14144140..117f9fdde78 100644 --- a/homeassistant/components/vacuum/const.py +++ b/homeassistant/components/vacuum/const.py @@ -27,7 +27,7 @@ class VacuumActivity(StrEnum): # These STATE_* constants are deprecated as of Home Assistant 2024.11. -# Please use the VacuumState enum instead. +# Please use the VacuumActivity enum instead. _DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumActivity.CLEANING, "2025.11") _DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumActivity.DOCKED, "2025.11") _DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 1b5fed2b7d3..532eb9581cd 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE @@ -50,29 +50,29 @@ ATTR_ZONE_REPEATER = "repeats" ATTR_TIMERS = "timers" STATE_CODE_TO_STATE = { - 1: VacuumState.IDLE, # "Starting" - 2: VacuumState.IDLE, # "Charger disconnected" - 3: VacuumState.IDLE, # "Idle" - 4: VacuumState.CLEANING, # "Remote control active" - 5: VacuumState.CLEANING, # "Cleaning" - 6: VacuumState.RETURNING, # "Returning home" - 7: VacuumState.CLEANING, # "Manual mode" - 8: VacuumState.DOCKED, # "Charging" - 9: VacuumState.ERROR, # "Charging problem" - 10: VacuumState.PAUSED, # "Paused" - 11: VacuumState.CLEANING, # "Spot cleaning" - 12: VacuumState.ERROR, # "Error" - 13: VacuumState.IDLE, # "Shutting down" - 14: VacuumState.DOCKED, # "Updating" - 15: VacuumState.RETURNING, # "Docking" - 16: VacuumState.CLEANING, # "Going to target" - 17: VacuumState.CLEANING, # "Zoned cleaning" - 18: VacuumState.CLEANING, # "Segment cleaning" - 22: VacuumState.DOCKED, # "Emptying the bin" on s7+ - 23: VacuumState.DOCKED, # "Washing the mop" on s7maxV - 26: VacuumState.RETURNING, # "Going to wash the mop" on s7maxV - 100: VacuumState.DOCKED, # "Charging complete" - 101: VacuumState.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" } @@ -206,7 +206,7 @@ class MiroboVacuum( ) -> None: """Initialize the Xiaomi vacuum cleaner robot handler.""" super().__init__(device, entry, unique_id, coordinator) - self._state: VacuumState | 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.""" @@ -214,12 +214,12 @@ class MiroboVacuum( self._handle_coordinator_update() @property - def vacuum_state(self) -> VacuumState | 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 VacuumState.ERROR + return VacuumActivity.ERROR return self._state diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index 8e2bf5acf6a..f910e6e53ac 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -22,7 +22,7 @@ from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - VacuumState, + VacuumActivity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -71,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 == VacuumState.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 == VacuumState.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 == VacuumState.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 == VacuumState.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 == VacuumState.DOCKED + assert state.state == VacuumActivity.DOCKED async def test_methods(hass: HomeAssistant) -> None: @@ -107,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 == VacuumState.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 == VacuumState.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 == VacuumState.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 == VacuumState.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 == VacuumState.RETURNING + assert state.state == VacuumActivity.RETURNING await common.async_set_fan_speed( hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_COMPLETE @@ -141,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 == VacuumState.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 == VacuumState.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 == VacuumState.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 == VacuumState.DOCKED + assert state.state == VacuumActivity.DOCKED async def test_unsupported_methods(hass: HomeAssistant) -> None: @@ -247,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 == VacuumState.IDLE + assert new_state_complete.state == VacuumActivity.IDLE diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 14108e6d16d..9e9c7015674 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -432,7 +432,7 @@ async def test_dock_vacuum(hass: HomeAssistant) -> None: assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None, None) trt = trait.DockTrait( - hass, State("vacuum.bla", vacuum.VacuumState.IDLE), BASIC_CONFIG + hass, State("vacuum.bla", vacuum.VacuumActivity.IDLE), BASIC_CONFIG ) assert trt.sync_attributes() == {} @@ -456,7 +456,7 @@ async def test_locate_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumState.IDLE, + vacuum.VacuumActivity.IDLE, {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.LOCATE}, ), BASIC_CONFIG, @@ -487,7 +487,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumState.DOCKED, + vacuum.VacuumActivity.DOCKED, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 100, @@ -513,7 +513,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumState.CLEANING, + vacuum.VacuumActivity.CLEANING, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 20, @@ -553,7 +553,7 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None: hass, State( "vacuum.bla", - vacuum.VacuumState.PAUSED, + vacuum.VacuumActivity.PAUSED, {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE}, ), BASIC_CONFIG, diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 1b1699f5282..0d19763e4c7 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -26,8 +26,8 @@ from homeassistant.components.vacuum import ( SERVICE_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -294,7 +294,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, - VacuumState.CLEANING, + VacuumActivity.CLEANING, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START @@ -305,7 +305,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( hass.states.async_set( entity_id, - VacuumState.DOCKED, + VacuumActivity.DOCKED, { ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index b23b5373579..1c8e0742b26 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import litterrobot from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_START, - VacuumState, + 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 == VacuumState.DOCKED + assert vacuum.state == VacuumActivity.DOCKED await hass.services.async_call( VACUUM_DOMAIN, diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 4bfe50a3839..69fb99d7d13 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -15,7 +15,7 @@ from homeassistant.components.vacuum import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_START, SERVICE_STOP, - VacuumState, + VacuumActivity, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -51,7 +51,7 @@ async def test_vacuum( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumState.DOCKED + assert vacuum.state == VacuumActivity.DOCKED assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID) @@ -93,21 +93,21 @@ async def test_vacuum_with_error( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumState.ERROR + assert vacuum.state == VacuumActivity.ERROR @pytest.mark.parametrize( ("robot_data", "expected_state"), [ - ({"displayCode": "DC_CAT_DETECT"}, VacuumState.DOCKED), - ({"isDFIFull": True}, VacuumState.ERROR), + ({"displayCode": "DC_CAT_DETECT"}, VacuumActivity.DOCKED), + ({"isDFIFull": True}, VacuumActivity.ERROR), ( {"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, - VacuumState.PAUSED, + VacuumActivity.PAUSED, ), ], ) -async def test_vacuum_states( +async def test_activitys( hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock, robot_data: dict[str, str | bool], @@ -151,7 +151,7 @@ async def test_commands( vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum - assert vacuum.state == VacuumState.DOCKED + assert vacuum.state == VacuumActivity.DOCKED extra = extra or {} data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})} diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index f90f323d199..c1c662048d7 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -27,7 +27,7 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, SERVICE_STOP, - VacuumState, + VacuumActivity, ) from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -312,7 +312,7 @@ async def test_status( }""" async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") - assert state.state == VacuumState.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" @@ -325,7 +325,7 @@ async def test_status( async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") - assert state.state == VacuumState.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" @@ -365,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 == VacuumState.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 @@ -379,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 == VacuumState.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 @@ -393,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 == VacuumState.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 diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 2437f8372b7..bfb2176026b 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -35,8 +35,8 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, + VacuumActivity, VacuumEntityFeature, - VacuumState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -157,7 +157,7 @@ async def test_simple_properties( assert entity assert state - assert state.state == VacuumState.CLEANING + assert state.state == VacuumActivity.CLEANING assert entity.unique_id == "AC000Wxxxxxxxxx" @@ -186,10 +186,10 @@ async def test_initial_attributes( @pytest.mark.parametrize( ("service", "target_state"), [ - (SERVICE_STOP, VacuumState.IDLE), - (SERVICE_PAUSE, VacuumState.PAUSED), - (SERVICE_RETURN_TO_BASE, VacuumState.RETURNING), - (SERVICE_START, VacuumState.CLEANING), + (SERVICE_STOP, VacuumActivity.IDLE), + (SERVICE_PAUSE, VacuumActivity.PAUSED), + (SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING), + (SERVICE_START, VacuumActivity.CLEANING), ], ) async def test_cleaning_states( diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index da44c1c8e2d..6053a2bd9ec 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import pytest from homeassistant import setup -from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumState +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 @@ -37,7 +37,7 @@ _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" }, ), ( - VacuumState.CLEANING, + VacuumActivity.CLEANING, 100, { "vacuum": { @@ -142,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, VacuumState.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, VacuumState.CLEANING, 100) + _verify(hass, VacuumActivity.CLEANING, 100) @pytest.mark.parametrize( @@ -363,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 == VacuumState.CLEANING - _verify(hass, VacuumState.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 @@ -374,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 == VacuumState.PAUSED - _verify(hass, VacuumState.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 @@ -385,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 == VacuumState.IDLE - _verify(hass, VacuumState.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 @@ -396,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 == VacuumState.RETURNING - _verify(hass, VacuumState.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 @@ -501,7 +501,7 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None: "input_select", { "input_select": { - "state": {"name": "State", "options": [VacuumState.CLEANING]} + "state": {"name": "State", "options": [VacuumActivity.CLEANING]} } }, ) @@ -519,7 +519,7 @@ async def _register_basic_vacuum(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumState.CLEANING, + "option": VacuumActivity.CLEANING, }, } } @@ -551,11 +551,11 @@ async def _register_components(hass: HomeAssistant) -> None: "state": { "name": "State", "options": [ - VacuumState.CLEANING, - VacuumState.DOCKED, - VacuumState.IDLE, - VacuumState.PAUSED, - VacuumState.RETURNING, + VacuumActivity.CLEANING, + VacuumActivity.DOCKED, + VacuumActivity.IDLE, + VacuumActivity.PAUSED, + VacuumActivity.RETURNING, ], }, "fan_speed": { @@ -575,7 +575,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumState.CLEANING, + "option": VacuumActivity.CLEANING, }, }, { @@ -591,7 +591,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumState.PAUSED, + "option": VacuumActivity.PAUSED, }, }, { @@ -607,7 +607,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumState.IDLE, + "option": VacuumActivity.IDLE, }, }, { @@ -623,7 +623,7 @@ async def _register_components(hass: HomeAssistant) -> None: "service": "input_select.select_option", "data": { "entity_id": _STATE_INPUT_SELECT, - "option": VacuumState.RETURNING, + "option": VacuumActivity.RETURNING, }, }, { diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 8e9ef3976eb..e58f21e387b 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -21,7 +21,7 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, - VacuumState, + VacuumActivity, ) from homeassistant.components.xiaomi_miio.const import ( CONF_FLOW_TYPE, @@ -263,7 +263,7 @@ async def test_xiaomi_vacuum_services( # Check state attributes state = hass.states.get(entity_id) - assert state.state == VacuumState.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" @@ -449,7 +449,7 @@ async def test_xiaomi_specific_services( # Check state attributes state = hass.states.get(entity_id) - assert state.state == VacuumState.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" From f832cacb9a0a9e030d12f7b5de19f156f74ee803 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:43:15 +0000 Subject: [PATCH 18/86] Update demo --- homeassistant/components/demo/vacuum.py | 29 ++++++++++--------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 0059f7c6a54..3dd945ab82e 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -87,16 +87,11 @@ class StateDemoVacuum(StateVacuumEntity): """Initialize the vacuum.""" self._attr_name = name self._attr_supported_features = supported_features - self._state = VacuumActivity.DOCKED + self._attr_activity = VacuumActivity.DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 - @property - def activity(self) -> VacuumActivity: - """Return the current state of the vacuum.""" - return self._state - @property def battery_level(self) -> int: """Return the current battery level of the vacuum.""" @@ -119,33 +114,33 @@ class StateDemoVacuum(StateVacuumEntity): def start(self) -> None: """Start or resume the cleaning task.""" - if self._state != VacuumActivity.CLEANING: - self._state = VacuumActivity.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 == VacuumActivity.CLEANING: - self._state = VacuumActivity.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 = VacuumActivity.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 = VacuumActivity.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 = VacuumActivity.CLEANING + self._attr_activity = VacuumActivity.CLEANING self._cleaned_area += 1.32 self._battery_level -= 1 self.schedule_update_ha_state() @@ -163,12 +158,12 @@ class StateDemoVacuum(StateVacuumEntity): "persistent_notification", service_data={"message": "I'm here!", "title": "Locate request"}, ) - self._state = VacuumActivity.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 = VacuumActivity.CLEANING + self._attr_activity = VacuumActivity.CLEANING self.async_write_ha_state() async def async_send_command( @@ -178,9 +173,9 @@ class StateDemoVacuum(StateVacuumEntity): **kwargs: Any, ) -> None: """Send a command to the vacuum.""" - self._state = VacuumActivity.IDLE + self._attr_activity = VacuumActivity.IDLE self.async_write_ha_state() def __set_state_to_dock(self, _: datetime) -> None: - self._state = VacuumActivity.DOCKED + self._attr_activity = VacuumActivity.DOCKED self.schedule_update_ha_state() From e53d165db5f269670622bb219a992d2302750e7c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 20:46:31 +0000 Subject: [PATCH 19/86] Mod roomba --- homeassistant/components/roomba/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/entity.py b/homeassistant/components/roomba/entity.py index ae9486f2254..57d84908550 100644 --- a/homeassistant/components/roomba/entity.py +++ b/homeassistant/components/roomba/entity.py @@ -169,7 +169,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1 @property - def vacuum_state(self) -> VacuumActivity: + def activity(self) -> VacuumActivity: """Return the state of the vacuum cleaner.""" return self._robot_state From 3ec070440555b5ca52703158404dcc237c13633b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 21:11:56 +0000 Subject: [PATCH 20/86] LG --- homeassistant/components/lg_thinq/vacuum.py | 40 ++++++++++----------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/lg_thinq/vacuum.py b/homeassistant/components/lg_thinq/vacuum.py index 138b9ba55bf..6cbb731869c 100644 --- a/homeassistant/components/lg_thinq/vacuum.py +++ b/homeassistant/components/lg_thinq/vacuum.py @@ -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 From bb4f13fdb5d126c2a6049258afade4813e799fbe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 21:19:41 +0000 Subject: [PATCH 21/86] Fix vacuum --- homeassistant/components/vacuum/__init__.py | 10 ++++---- homeassistant/components/vacuum/const.py | 8 +++--- tests/components/vacuum/test_init.py | 28 +++++++++++++++++---- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index f03ff4833fa..f4548b96c6b 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -77,8 +77,8 @@ DEFAULT_NAME = "Vacuum cleaner robot" # These STATE_* constants are deprecated as of Home Assistant 2024.11. # Please use the VacuumActivity enum instead. -_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2025.11") -_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2025.11") +_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2025.12") +_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2025.12") class VacuumEntityFeature(IntFlag): @@ -286,9 +286,9 @@ class StateVacuumEntity( Integrations should implement activity instead of using state directly. """ report( - "is setting state directly which will stop working in HA Core 2025.11." - f" Entity {self.entity_id} ({type(self)}) should implement the 'activity' property and" - " return its state using the VacuumActivity enum, please ", + "is setting state directly which will stop working in HA Core 2025.12." + f" Entity {self.entity_id} ({type(self)}) should implement the 'activity'" + " property and return its state using the VacuumActivity enum.", error_if_core=True, error_if_integration=False, ) diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py index 117f9fdde78..8f80b35bbd9 100644 --- a/homeassistant/components/vacuum/const.py +++ b/homeassistant/components/vacuum/const.py @@ -28,12 +28,12 @@ class VacuumActivity(StrEnum): # These STATE_* constants are deprecated as of Home Assistant 2024.11. # Please use the VacuumActivity enum instead. -_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumActivity.CLEANING, "2025.11") -_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumActivity.DOCKED, "2025.11") +_DEPRECATED_STATE_CLEANING = DeprecatedConstantEnum(VacuumActivity.CLEANING, "2025.12") +_DEPRECATED_STATE_DOCKED = DeprecatedConstantEnum(VacuumActivity.DOCKED, "2025.12") _DEPRECATED_STATE_RETURNING = DeprecatedConstantEnum( - VacuumActivity.RETURNING, "2025.11" + VacuumActivity.RETURNING, "2025.12" ) -_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumActivity.ERROR, "2025.11") +_DEPRECATED_STATE_ERROR = DeprecatedConstantEnum(VacuumActivity.ERROR, "2025.12") STATES = [cls.value for cls in VacuumActivity] diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index e1f240b37cb..5b8e2c57a46 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -26,6 +26,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import frame from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MockVacuum, help_async_setup_entry_init, help_async_unload_entry @@ -319,9 +320,13 @@ async def test_vacuum_not_log_deprecated_state_warning( """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 "Entities should implement the 'activity' property and" not in caplog.text + assert ( + "should implement the 'activity' property and return its state using the VacuumActivity enum" + not in caplog.text + ) +@patch.object(frame, "_REPORTED_INTEGRATIONS", set()) async def test_vacuum_log_deprecated_state_warning_using_state_prop( hass: HomeAssistant, config_flow_fixture: None, @@ -383,9 +388,13 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( state = hass.states.get(entity.entity_id) assert state is not None - assert "Entities should implement the 'activity' property and" in caplog.text + assert ( + "should implement the 'activity' property and return its state using the VacuumActivity enum" + in caplog.text + ) +@patch.object(frame, "_REPORTED_INTEGRATIONS", set()) async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( hass: HomeAssistant, config_flow_fixture: None, @@ -446,7 +455,10 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( state = hass.states.get(entity.entity_id) assert state is not None - assert "Entities should implement the 'activity' property and" not in caplog.text + assert ( + "should implement the 'activity' property and return its state using the VacuumActivity enum" + not in caplog.text + ) with patch.object( MockLegacyVacuum, @@ -455,7 +467,10 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( ): await async_start(hass, entity.entity_id) - assert "Entities should implement the 'activity' property and" in caplog.text + assert ( + "should implement the 'activity' property and return its state using the VacuumActivity enum" + in caplog.text + ) caplog.clear() with patch.object( MockLegacyVacuum, @@ -464,4 +479,7 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( ): await async_start(hass, entity.entity_id) # Test we only log once - assert "Entities should implement the 'activity' property and" not in caplog.text + assert ( + "should implement the 'activity' property and return its state using the VacuumActivity enum" + not in caplog.text + ) From c1581bc18b67b5bd2786107aa9537fc2722e3575 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 21:25:06 +0000 Subject: [PATCH 22/86] Fix Matter --- homeassistant/components/matter/vacuum.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index 2ecd7128df6..e98e1ad0bbd 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -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: From 3eab72b2aab4d8184e351953322f4a1c300d331e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 11 Nov 2024 23:02:48 +0100 Subject: [PATCH 23/86] Improve exception handling in Nord Pool (#130386) * Improve exception handling in Nord Pool * Improve auth string * Remove auth --- .../components/nordpool/config_flow.py | 14 +++--- .../components/nordpool/coordinator.py | 12 ++--- tests/components/nordpool/test_config_flow.py | 45 ++----------------- tests/components/nordpool/test_coordinator.py | 38 +++++++--------- 4 files changed, 31 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/nordpool/config_flow.py b/homeassistant/components/nordpool/config_flow.py index a9a834d8225..1d75d825e47 100644 --- a/homeassistant/components/nordpool/config_flow.py +++ b/homeassistant/components/nordpool/config_flow.py @@ -4,7 +4,12 @@ from __future__ import annotations from typing import Any -from pynordpool import Currency, NordPoolClient, NordPoolError +from pynordpool import ( + Currency, + NordPoolClient, + NordPoolEmptyResponseError, + NordPoolError, +) from pynordpool.const import AREAS import voluptuous as vol @@ -53,17 +58,16 @@ async def test_api(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, """Test fetch data from Nord Pool.""" client = NordPoolClient(async_get_clientsession(hass)) try: - data = await client.async_get_delivery_period( + await client.async_get_delivery_period( dt_util.now(), Currency(user_input[CONF_CURRENCY]), user_input[CONF_AREAS], ) + except NordPoolEmptyResponseError: + return {"base": "no_data"} except NordPoolError: return {"base": "cannot_connect"} - if not data.raw: - return {"base": "no_data"} - return {} diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py index 27016ae2b4b..fa4e9ca2548 100644 --- a/homeassistant/components/nordpool/coordinator.py +++ b/homeassistant/components/nordpool/coordinator.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING from pynordpool import ( Currency, DeliveryPeriodData, - NordPoolAuthenticationError, NordPoolClient, + NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -19,7 +19,7 @@ from homeassistant.const import CONF_CURRENCY from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util from .const import CONF_AREAS, DOMAIN, LOGGER @@ -75,8 +75,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]): Currency(self.config_entry.data[CONF_CURRENCY]), self.config_entry.data[CONF_AREAS], ) - except NordPoolAuthenticationError as error: - LOGGER.error("Authentication error: %s", error) + except NordPoolEmptyResponseError as error: + LOGGER.debug("Empty response error: %s", error) self.async_set_update_error(error) return except NordPoolResponseError as error: @@ -88,8 +88,4 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]): self.async_set_update_error(error) return - if not data.raw: - self.async_set_update_error(UpdateFailed("No data")) - return - self.async_set_updated_data(data) diff --git a/tests/components/nordpool/test_config_flow.py b/tests/components/nordpool/test_config_flow.py index d17db619b02..cfdfc63aca7 100644 --- a/tests/components/nordpool/test_config_flow.py +++ b/tests/components/nordpool/test_config_flow.py @@ -2,13 +2,12 @@ from __future__ import annotations -from dataclasses import replace from unittest.mock import patch from pynordpool import ( DeliveryPeriodData, - NordPoolAuthenticationError, NordPoolConnectionError, + NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -71,7 +70,7 @@ async def test_single_config_entry( ("error_message", "p_error"), [ (NordPoolConnectionError, "cannot_connect"), - (NordPoolAuthenticationError, "cannot_connect"), + (NordPoolEmptyResponseError, "no_data"), (NordPoolError, "cannot_connect"), (NordPoolResponseError, "cannot_connect"), ], @@ -116,44 +115,6 @@ async def test_cannot_connect( assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") -async def test_empty_data(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: - """Test empty data error.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER - - invalid_data = replace(get_data, raw={}) - - with patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=invalid_data, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=ENTRY_CONFIG, - ) - - assert result["errors"] == {"base": "no_data"} - - with patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=ENTRY_CONFIG, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Nord Pool" - assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} - - @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") async def test_reconfigure( hass: HomeAssistant, @@ -193,7 +154,7 @@ async def test_reconfigure( ("error_message", "p_error"), [ (NordPoolConnectionError, "cannot_connect"), - (NordPoolAuthenticationError, "cannot_connect"), + (NordPoolEmptyResponseError, "no_data"), (NordPoolError, "cannot_connect"), (NordPoolResponseError, "cannot_connect"), ], diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index 9cff34adb1f..d2d912b1b99 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -9,6 +9,7 @@ from freezegun.api import FrozenDateTimeFactory from pynordpool import ( DeliveryPeriodData, NordPoolAuthenticationError, + NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -18,14 +19,13 @@ from homeassistant.components.nordpool.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed -@pytest.mark.freeze_time("2024-11-05T12:00:00+00:00") +@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") async def test_coordinator( hass: HomeAssistant, get_data: DeliveryPeriodData, @@ -51,7 +51,7 @@ async def test_coordinator( await hass.async_block_till_done() mock_data.assert_called_once() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.94949" + assert state.state == "0.92737" mock_data.reset_mock() mock_data.side_effect = NordPoolError("error") @@ -74,6 +74,17 @@ async def test_coordinator( assert "Authentication error" in caplog.text mock_data.reset_mock() + assert "Empty response" not in caplog.text + mock_data.side_effect = NordPoolEmptyResponseError("Empty response") + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + mock_data.assert_called_once() + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == STATE_UNAVAILABLE + assert "Empty response" in caplog.text + mock_data.reset_mock() + assert "Response error" not in caplog.text mock_data.side_effect = NordPoolResponseError("Response error") freezer.tick(timedelta(hours=1)) @@ -85,25 +96,6 @@ async def test_coordinator( assert "Response error" in caplog.text mock_data.reset_mock() - mock_data.return_value = DeliveryPeriodData( - raw={}, - requested_date="2024-11-05", - updated_at=dt_util.utcnow(), - entries=[], - block_prices=[], - currency="SEK", - exchange_rate=1, - area_average={}, - ) - mock_data.side_effect = None - freezer.tick(timedelta(hours=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == STATE_UNAVAILABLE - mock_data.reset_mock() - mock_data.return_value = get_data mock_data.side_effect = None freezer.tick(timedelta(hours=1)) @@ -111,4 +103,4 @@ async def test_coordinator( await hass.async_block_till_done() mock_data.assert_called_once() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.81983" + assert state.state == "1.81645" From 60bf0f6b06b7c9901a02f74ac8869378f3df4409 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 12 Nov 2024 16:26:28 +0900 Subject: [PATCH 24/86] Fix fan's warning TURN_ON, TURN_OFF (#130327) Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/fan.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py index 187cc74b3eb..edcadf2598a 100644 --- a/homeassistant/components/lg_thinq/fan.py +++ b/homeassistant/components/lg_thinq/fan.py @@ -72,8 +72,11 @@ class ThinQFanEntity(ThinQEntity, FanEntity): super().__init__(coordinator, entity_description, property_id) self._ordered_named_fan_speeds = [] - self._attr_supported_features |= FanEntityFeature.SET_SPEED - + self._attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) if (fan_modes := self.data.fan_modes) is not None: self._attr_speed_count = len(fan_modes) if self.speed_count == 4: @@ -98,7 +101,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): self._attr_percentage = 0 _LOGGER.debug( - "[%s:%s] update status: %s -> %s (percntage=%s)", + "[%s:%s] update status: %s -> %s (percentage=%s)", self.coordinator.device_name, self.property_id, self.data.is_on, @@ -120,7 +123,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): return _LOGGER.debug( - "[%s:%s] async_set_percentage. percntage=%s, value=%s", + "[%s:%s] async_set_percentage. percentage=%s, value=%s", self.coordinator.device_name, self.property_id, percentage, From 22aed924618f2c9d63736985f57d2af2cb8468fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Nov 2024 01:29:01 -0600 Subject: [PATCH 25/86] Bump aiohttp to 3.11.0rc1 (#130320) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ec2dc977989..a40c8745877 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.1.1 -aiohttp==3.11.0rc0 +aiohttp==3.11.0rc1 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 4a9192d7767..adc85c0f4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.0rc0", + "aiohttp==3.11.0rc1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 19f8ac9ee22..53d6b13a4ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.0rc0 +aiohttp==3.11.0rc1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 7045b776b6cd47ee06548f4687b7a34ec1c1c4b8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:25:13 +0100 Subject: [PATCH 26/86] Use report_usage in helpers (#130365) --- homeassistant/helpers/config_validation.py | 12 ++++++------ homeassistant/helpers/event.py | 6 +++--- homeassistant/helpers/service.py | 6 +++--- homeassistant/helpers/template.py | 6 +++--- homeassistant/helpers/update_coordinator.py | 12 ++++-------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 81ac10f86cc..2b35ebade76 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -719,14 +719,14 @@ def template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value should be a string") if not (hass := _async_get_hass_or_none()): # pylint: disable-next=import-outside-toplevel - from .frame import report + from .frame import ReportBehavior, report_usage - report( + report_usage( ( "validates schema outside the event loop, " "which will stop working in HA Core 2025.10" ), - error_if_core=False, + core_behavior=ReportBehavior.LOG, ) template_value = template_helper.Template(str(value), hass) @@ -748,14 +748,14 @@ def dynamic_template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value does not contain a dynamic template") if not (hass := _async_get_hass_or_none()): # pylint: disable-next=import-outside-toplevel - from .frame import report + from .frame import ReportBehavior, report_usage - report( + report_usage( ( "validates schema outside the event loop, " "which will stop working in HA Core 2025.10" ), - error_if_core=False, + core_behavior=ReportBehavior.LOG, ) template_value = template_helper.Template(str(value), hass) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 02ea8103192..61a798dbd75 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -997,14 +997,14 @@ class TrackTemplateResultInfo: continue # pylint: disable-next=import-outside-toplevel - from .frame import report + from .frame import ReportBehavior, report_usage - report( + report_usage( ( "calls async_track_template_result with template without hass, " "which will stop working in HA Core 2025.10" ), - error_if_core=False, + core_behavior=ReportBehavior.LOG, ) track_template_.template.hass = hass diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 33e8f3d3d6e..e3da52604cb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1277,14 +1277,14 @@ def async_register_entity_service( schema = cv.make_entity_service_schema(schema) elif not cv.is_entity_service_schema(schema): # pylint: disable-next=import-outside-toplevel - from .frame import report + from .frame import ReportBehavior, report_usage - report( + report_usage( ( "registers an entity service with a non entity service schema " "which will stop working in HA Core 2025.9" ), - error_if_core=False, + core_behavior=ReportBehavior.LOG, ) service_func: str | HassJob[..., Any] diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 753464c35d5..2eab666bbd4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -515,18 +515,18 @@ class Template: will be non optional in Home Assistant Core 2025.10. """ # pylint: disable-next=import-outside-toplevel - from .frame import report + from .frame import ReportBehavior, report_usage if not isinstance(template, str): raise TypeError("Expected template to be a string") if not hass: - report( + report_usage( ( "creates a template object without passing hass, " "which will stop working in HA Core 2025.10" ), - error_if_core=False, + core_behavior=ReportBehavior.LOG, ) self.template: str = template.strip() diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f5c2a2a1288..87d55891e90 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -29,7 +29,7 @@ from homeassistant.util.dt import utcnow from . import entity, event from .debounce import Debouncer -from .frame import report +from .frame import report_usage from .typing import UNDEFINED, UndefinedType REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 @@ -286,24 +286,20 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): to ensure that multiple retries do not cause log spam. """ if self.config_entry is None: - report( + report_usage( "uses `async_config_entry_first_refresh`, which is only supported " "for coordinators with a config entry and will stop working in " - "Home Assistant 2025.11", - error_if_core=True, - error_if_integration=False, + "Home Assistant 2025.11" ) elif ( self.config_entry.state is not config_entries.ConfigEntryState.SETUP_IN_PROGRESS ): - report( + report_usage( "uses `async_config_entry_first_refresh`, which is only supported " f"when entry state is {config_entries.ConfigEntryState.SETUP_IN_PROGRESS}, " f"but it is in state {self.config_entry.state}, " "This will stop working in Home Assistant 2025.11", - error_if_core=True, - error_if_integration=False, ) if await self.__wrap_async_setup(): await self._async_refresh( From 7758d8ba48e8d19674a39b10c48a58ef31f5281b Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Tue, 12 Nov 2024 11:42:25 +0100 Subject: [PATCH 27/86] Add switch platform to eq3btsmart (#130363) --- .../components/eq3btsmart/__init__.py | 1 + homeassistant/components/eq3btsmart/const.py | 3 + .../components/eq3btsmart/icons.json | 32 +++++++ .../components/eq3btsmart/strings.json | 11 +++ homeassistant/components/eq3btsmart/switch.py | 94 +++++++++++++++++++ 5 files changed, 141 insertions(+) create mode 100644 homeassistant/components/eq3btsmart/icons.json create mode 100644 homeassistant/components/eq3btsmart/switch.py diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py index 78296c70cef..86c555ec151 100644 --- a/homeassistant/components/eq3btsmart/__init__.py +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -21,6 +21,7 @@ from .models import Eq3Config, Eq3ConfigEntryData PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.SWITCH, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py index 33d8e6b3cee..64bc1cf497c 100644 --- a/homeassistant/components/eq3btsmart/const.py +++ b/homeassistant/components/eq3btsmart/const.py @@ -21,6 +21,9 @@ DEVICE_MODEL = "CC-RT-BLE-EQ" ENTITY_KEY_DST = "dst" ENTITY_KEY_BATTERY = "battery" ENTITY_KEY_WINDOW = "window" +ENTITY_KEY_LOCK = "lock" +ENTITY_KEY_BOOST = "boost" +ENTITY_KEY_AWAY = "away" GET_DEVICE_TIMEOUT = 5 # seconds diff --git a/homeassistant/components/eq3btsmart/icons.json b/homeassistant/components/eq3btsmart/icons.json new file mode 100644 index 00000000000..fb0862f14bc --- /dev/null +++ b/homeassistant/components/eq3btsmart/icons.json @@ -0,0 +1,32 @@ +{ + "entity": { + "binary_sensor": { + "dst": { + "default": "mdi:sun-clock", + "state": { + "off": "mdi:sun-clock-outline" + } + } + }, + "switch": { + "away": { + "default": "mdi:home-account", + "state": { + "on": "mdi:home-export" + } + }, + "lock": { + "default": "mdi:lock", + "state": { + "off": "mdi:lock-off" + } + }, + "boost": { + "default": "mdi:fire", + "state": { + "off": "mdi:fire-off" + } + } + } + } +} diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json index c911be099d5..03c3b21b964 100644 --- a/homeassistant/components/eq3btsmart/strings.json +++ b/homeassistant/components/eq3btsmart/strings.json @@ -24,6 +24,17 @@ "dst": { "name": "Daylight saving time" } + }, + "switch": { + "lock": { + "name": "Lock" + }, + "boost": { + "name": "Boost" + }, + "away": { + "name": "Away" + } } } } diff --git a/homeassistant/components/eq3btsmart/switch.py b/homeassistant/components/eq3btsmart/switch.py new file mode 100644 index 00000000000..7525d8ca494 --- /dev/null +++ b/homeassistant/components/eq3btsmart/switch.py @@ -0,0 +1,94 @@ +"""Platform for eq3 switch entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from eq3btsmart import Thermostat +from eq3btsmart.models import Status + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import Eq3ConfigEntry +from .const import ENTITY_KEY_AWAY, ENTITY_KEY_BOOST, ENTITY_KEY_LOCK +from .entity import Eq3Entity + + +@dataclass(frozen=True, kw_only=True) +class Eq3SwitchEntityDescription(SwitchEntityDescription): + """Entity description for eq3 switch entities.""" + + toggle_func: Callable[[Thermostat], Callable[[bool], Awaitable[None]]] + value_func: Callable[[Status], bool] + + +SWITCH_ENTITY_DESCRIPTIONS = [ + Eq3SwitchEntityDescription( + key=ENTITY_KEY_LOCK, + translation_key=ENTITY_KEY_LOCK, + toggle_func=lambda thermostat: thermostat.async_set_locked, + value_func=lambda status: status.is_locked, + ), + Eq3SwitchEntityDescription( + key=ENTITY_KEY_BOOST, + translation_key=ENTITY_KEY_BOOST, + toggle_func=lambda thermostat: thermostat.async_set_boost, + value_func=lambda status: status.is_boost, + ), + Eq3SwitchEntityDescription( + key=ENTITY_KEY_AWAY, + translation_key=ENTITY_KEY_AWAY, + toggle_func=lambda thermostat: thermostat.async_set_away, + value_func=lambda status: status.is_away, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: Eq3ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the entry.""" + + async_add_entities( + Eq3SwitchEntity(entry, entity_description) + for entity_description in SWITCH_ENTITY_DESCRIPTIONS + ) + + +class Eq3SwitchEntity(Eq3Entity, SwitchEntity): + """Base class for eq3 switch entities.""" + + entity_description: Eq3SwitchEntityDescription + + def __init__( + self, + entry: Eq3ConfigEntry, + entity_description: Eq3SwitchEntityDescription, + ) -> None: + """Initialize the entity.""" + + super().__init__(entry, entity_description.key) + self.entity_description = entity_description + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + + await self.entity_description.toggle_func(self._thermostat)(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + + await self.entity_description.toggle_func(self._thermostat)(False) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + + if TYPE_CHECKING: + assert self._thermostat.status is not None + + return self.entity_description.value_func(self._thermostat.status) From cb9cc0f801118ae73e2cef959fdec274cd645293 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 12 Nov 2024 11:53:14 +0100 Subject: [PATCH 28/86] Go2rtc bump and set ffmpeg logs to debug (#130371) --- Dockerfile | 2 +- homeassistant/components/go2rtc/__init__.py | 83 ++------ homeassistant/components/go2rtc/const.py | 1 - homeassistant/components/go2rtc/server.py | 8 +- script/hassfest/docker.py | 2 +- tests/components/go2rtc/test_init.py | 223 +++----------------- 6 files changed, 51 insertions(+), 268 deletions(-) diff --git a/Dockerfile b/Dockerfile index 903a121c032..15574192093 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ RUN \ "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \ esac \ - && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ + && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && chmod +x /bin/go2rtc \ # Verify go2rtc can be executed && go2rtc --version diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 04b5b9f9317..fc91ef5e546 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -1,8 +1,5 @@ """The go2rtc component.""" -from __future__ import annotations - -from dataclasses import dataclass import logging import shutil @@ -41,13 +38,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import ( - CONF_DEBUG_UI, - DEBUG_UI_URL_MESSAGE, - DOMAIN, - HA_MANAGED_RTSP_PORT, - HA_MANAGED_URL, -) +from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL from .server import Server _LOGGER = logging.getLogger(__name__) @@ -94,22 +85,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN) +_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) -@dataclass(frozen=True) -class Go2RtcData: - """Data for go2rtc.""" - - url: str - managed: bool - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up WebRTC.""" url: str | None = None - managed = False if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: await _remove_go2rtc_entries(hass) return True @@ -144,9 +126,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) url = HA_MANAGED_URL - managed = True - hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed) + hass.data[_DATA_GO2RTC] = url discovery_flow.async_create_flow( hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} ) @@ -161,32 +142,28 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up go2rtc from a config entry.""" - data = hass.data[_DATA_GO2RTC] + url = hass.data[_DATA_GO2RTC] # Validate the server URL try: - client = Go2RtcRestClient(async_get_clientsession(hass), data.url) + client = Go2RtcRestClient(async_get_clientsession(hass), url) await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( - f"Could not connect to go2rtc instance on {data.url}" + f"Could not connect to go2rtc instance on {url}" ) from err - _LOGGER.warning( - "Could not connect to go2rtc instance on %s (%s)", data.url, err - ) + _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False except Go2RtcVersionError as err: raise ConfigEntryNotReady( f"The go2rtc server version is not supported, {err}" ) from err except Exception as err: # noqa: BLE001 - _LOGGER.warning( - "Could not connect to go2rtc instance on %s (%s)", data.url, err - ) + _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False - provider = WebRTCProvider(hass, data) + provider = WebRTCProvider(hass, url) async_register_webrtc_provider(hass, provider) return True @@ -204,12 +181,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None: class WebRTCProvider(CameraWebRTCProvider): """WebRTC provider.""" - def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None: + def __init__(self, hass: HomeAssistant, url: str) -> None: """Initialize the WebRTC provider.""" self._hass = hass - self._data = data + self._url = url self._session = async_get_clientsession(hass) - self._rest_client = Go2RtcRestClient(self._session, data.url) + self._rest_client = Go2RtcRestClient(self._session, url) self._sessions: dict[str, Go2RtcWsClient] = {} @property @@ -231,7 +208,7 @@ class WebRTCProvider(CameraWebRTCProvider): ) -> None: """Handle the WebRTC offer and return the answer via the provided callback.""" self._sessions[session_id] = ws_client = Go2RtcWsClient( - self._session, self._data.url, source=camera.entity_id + self._session, self._url, source=camera.entity_id ) if not (stream_source := await camera.stream_source()): @@ -242,34 +219,18 @@ class WebRTCProvider(CameraWebRTCProvider): streams = await self._rest_client.streams.list() - if self._data.managed: - # HA manages the go2rtc instance - stream_original_name = f"{camera.entity_id}_original" - stream_redirect_sources = [ - f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}", - f"ffmpeg:{stream_original_name}#audio=opus", - ] - - if ( - (stream_org := streams.get(stream_original_name)) is None - or not any( - stream_source == producer.url for producer in stream_org.producers - ) - or (stream_redirect := streams.get(camera.entity_id)) is None - or stream_redirect_sources != [p.url for p in stream_redirect.producers] - ): - await self._rest_client.streams.add(stream_original_name, stream_source) - await self._rest_client.streams.add( - camera.entity_id, stream_redirect_sources - ) - - # go2rtc instance is managed outside HA - elif (stream_org := streams.get(camera.entity_id)) is None or not any( - stream_source == producer.url for producer in stream_org.producers + if (stream := streams.get(camera.entity_id)) is None or not any( + stream_source == producer.url for producer in stream.producers ): await self._rest_client.streams.add( camera.entity_id, - [stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"], + [ + stream_source, + # We are setting any ffmpeg rtsp related logs to debug + # Connection problems to the camera will be logged by the first stream + # Therefore setting it to debug will not hide any important logs + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) @callback diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index 3c4dc9a9500..d33ae3e3897 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,4 +6,3 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" -HA_MANAGED_RTSP_PORT = 18554 diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index 91f4433546c..6699ee4d8a2 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL +from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL _LOGGER = logging.getLogger(__name__) _TERMINATE_TIMEOUT = 5 @@ -33,7 +33,7 @@ api: listen: "{api_ip}:{api_port}" rtsp: - listen: "127.0.0.1:{rtsp_port}" + listen: "127.0.0.1:18554" webrtc: listen: ":18555/tcp" @@ -68,9 +68,7 @@ def _create_temp_file(api_ip: str) -> str: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: file.write( _GO2RTC_CONFIG_FORMAT.format( - api_ip=api_ip, - api_port=HA_MANAGED_API_PORT, - rtsp_port=HA_MANAGED_RTSP_PORT, + api_ip=api_ip, api_port=HA_MANAGED_API_PORT ).encode() ) return file.name diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 083cdaba1a9..9d38d8f7128 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -112,7 +112,7 @@ LABEL "com.github.actions.icon"="terminal" LABEL "com.github.actions.color"="gray-dark" """ -_GO2RTC_VERSION = "1.9.6" +_GO2RTC_VERSION = "1.9.7" def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index ec586776142..9388110366e 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Generator import logging from typing import NamedTuple -from unittest.mock import AsyncMock, Mock, call, patch +from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from go2rtc_client import Stream @@ -238,7 +238,11 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + entity_id, + [ + "rtsp://stream", + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) # Stream exists but the source is different @@ -252,7 +256,11 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + entity_id, + [ + "rtsp://stream", + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) # If the stream is already added, the stream should not be added again. @@ -296,7 +304,7 @@ async def _test_setup_and_signaling( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_managed( +async def test_setup_go_binary( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -308,131 +316,15 @@ async def test_setup_managed( config: ConfigType, ui_enabled: bool, ) -> None: - """Test the go2rtc setup with managed go2rtc instance.""" + """Test the go2rtc config entry with binary.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry - camera = init_test_integration - entity_id = camera.entity_id - stream_name_original = f"{camera.entity_id}_original" - assert camera.frontend_stream_type == StreamType.HLS + def after_setup() -> None: + server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) + server_start.assert_called_once() - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done(wait_background_tasks=True) - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - assert config_entries[0].state == ConfigEntryState.LOADED - server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) - server_start.assert_called_once() - - receive_message_callback = Mock(spec_set=WebRTCSendMessage) - - async def test() -> None: - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - ws_client.send.assert_called_once_with( - WebRTCOffer( - OFFER_SDP, - camera.async_get_webrtc_client_configuration().configuration.ice_servers, - ) - ) - ws_client.subscribe.assert_called_once() - - # Simulate the answer from the go2rtc server - callback = ws_client.subscribe.call_args[0][0] - callback(WebRTCAnswer(ANSWER_SDP)) - receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) - - await test() - - stream_added_calls = [ - call(stream_name_original, "rtsp://stream"), - call( - entity_id, - [ - f"rtsp://127.0.0.1:18554/{stream_name_original}", - f"ffmpeg:{stream_name_original}#audio=opus", - ], - ), - ] - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream original missing - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream original source different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://different")]), - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream source different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://stream")]), - entity_id: Stream([Producer("rtsp://different")]), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # If the stream is already added, the stream should not be added again. - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://stream")]), - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_not_called() - assert isinstance(camera._webrtc_provider, WebRTCProvider) - - # Set stream source to None and provider should be skipped - rest_client.streams.list.return_value = {} - receive_message_callback.reset_mock() - camera.set_stream_source(None) - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - receive_message_callback.assert_called_once_with( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") + await _test_setup_and_signaling( + hass, rest_client, ws_client, config, after_setup, init_test_integration ) await hass.async_stop() @@ -448,7 +340,7 @@ async def test_setup_managed( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_self_hosted( +async def test_setup_go( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -458,83 +350,16 @@ async def test_setup_self_hosted( mock_is_docker_env: Mock, has_go2rtc_entry: bool, ) -> None: - """Test the go2rtc with selfhosted go2rtc instance.""" + """Test the go2rtc config entry without binary.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} - camera = init_test_integration - entity_id = camera.entity_id - assert camera.frontend_stream_type == StreamType.HLS + def after_setup() -> None: + server.assert_not_called() - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done(wait_background_tasks=True) - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - assert config_entries[0].state == ConfigEntryState.LOADED - server.assert_not_called() - - receive_message_callback = Mock(spec_set=WebRTCSendMessage) - - async def test() -> None: - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - ws_client.send.assert_called_once_with( - WebRTCOffer( - OFFER_SDP, - camera.async_get_webrtc_client_configuration().configuration.ice_servers, - ) - ) - ws_client.subscribe.assert_called_once() - - # Simulate the answer from the go2rtc server - callback = ws_client.subscribe.call_args[0][0] - callback(WebRTCAnswer(ANSWER_SDP)) - receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) - - await test() - - rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] - ) - - # Stream exists but the source is different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream([Producer("rtsp://different")]) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] - ) - - # If the stream is already added, the stream should not be added again. - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream([Producer("rtsp://stream")]) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_not_called() - assert isinstance(camera._webrtc_provider, WebRTCProvider) - - # Set stream source to None and provider should be skipped - rest_client.streams.list.return_value = {} - receive_message_callback.reset_mock() - camera.set_stream_source(None) - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - receive_message_callback.assert_called_once_with( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") + await _test_setup_and_signaling( + hass, rest_client, ws_client, config, after_setup, init_test_integration ) mock_get_binary.assert_not_called() From ac0c75a598e4e7ee2c27b37e19a9ec5cefb8cd5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 12 Nov 2024 15:27:53 +0100 Subject: [PATCH 29/86] Add upload capability to the backup integration (#128546) * Add upload capability to the backup integration * Limit context switch * rename * coverage for http * Test receiving a backup file * Update test_manager.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/backup/http.py | 37 ++++++++++-- homeassistant/components/backup/manager.py | 70 ++++++++++++++++++++++ tests/components/backup/test_http.py | 57 +++++++++++++++++- tests/components/backup/test_manager.py | 38 +++++++++++- 4 files changed, 195 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index 4cc4e61c9e4..42693035bd3 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -2,23 +2,26 @@ from __future__ import annotations +import asyncio from http import HTTPStatus +from typing import cast +from aiohttp import BodyPartReader from aiohttp.hdrs import CONTENT_DISPOSITION from aiohttp.web import FileResponse, Request, Response -from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify -from .const import DOMAIN -from .manager import BaseBackupManager +from .const import DATA_MANAGER @callback def async_register_http_views(hass: HomeAssistant) -> None: """Register the http views.""" hass.http.register_view(DownloadBackupView) + hass.http.register_view(UploadBackupView) class DownloadBackupView(HomeAssistantView): @@ -36,7 +39,7 @@ class DownloadBackupView(HomeAssistantView): if not request["hass_user"].is_admin: return Response(status=HTTPStatus.UNAUTHORIZED) - manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN] + manager = request.app[KEY_HASS].data[DATA_MANAGER] backup = await manager.async_get_backup(slug=slug) if backup is None or not backup.path.exists(): @@ -48,3 +51,29 @@ class DownloadBackupView(HomeAssistantView): CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" }, ) + + +class UploadBackupView(HomeAssistantView): + """Generate backup view.""" + + url = "/api/backup/upload" + name = "api:backup:upload" + + @require_admin + async def post(self, request: Request) -> Response: + """Upload a backup file.""" + manager = request.app[KEY_HASS].data[DATA_MANAGER] + reader = await request.multipart() + contents = cast(BodyPartReader, await reader.next()) + + try: + await manager.async_receive_backup(contents=contents) + except OSError as err: + return Response( + body=f"Can't write backup file {err}", + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + except asyncio.CancelledError: + return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR) + + return Response(status=HTTPStatus.CREATED) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 8265dade3aa..4300f75eed0 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -9,11 +9,15 @@ import hashlib import io import json from pathlib import Path +from queue import SimpleQueue +import shutil import tarfile from tarfile import TarError +from tempfile import TemporaryDirectory import time from typing import Any, Protocol, cast +import aiohttp from securetar import SecureTarFile, atomic_contents_add from homeassistant.backup_restore import RESTORE_BACKUP_FILE @@ -147,6 +151,15 @@ class BaseBackupManager(abc.ABC): async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None: """Remove a backup.""" + @abc.abstractmethod + async def async_receive_backup( + self, + *, + contents: aiohttp.BodyPartReader, + **kwargs: Any, + ) -> None: + """Receive and store a backup file from upload.""" + class BackupManager(BaseBackupManager): """Backup manager for the Backup integration.""" @@ -222,6 +235,63 @@ class BackupManager(BaseBackupManager): LOGGER.debug("Removed backup located at %s", backup.path) self.backups.pop(slug) + async def async_receive_backup( + self, + *, + contents: aiohttp.BodyPartReader, + **kwargs: Any, + ) -> None: + """Receive and store a backup file from upload.""" + queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = ( + SimpleQueue() + ) + temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory) + target_temp_file = Path( + temp_dir_handler.name, contents.filename or "backup.tar" + ) + + def _sync_queue_consumer() -> None: + with target_temp_file.open("wb") as file_handle: + while True: + if (_chunk_future := queue.get()) is None: + break + _chunk, _future = _chunk_future + if _future is not None: + self.hass.loop.call_soon_threadsafe(_future.set_result, None) + file_handle.write(_chunk) + + fut: asyncio.Future[None] | None = None + try: + fut = self.hass.async_add_executor_job(_sync_queue_consumer) + megabytes_sending = 0 + while chunk := await contents.read_chunk(BUF_SIZE): + megabytes_sending += 1 + if megabytes_sending % 5 != 0: + queue.put_nowait((chunk, None)) + continue + + chunk_future = self.hass.loop.create_future() + queue.put_nowait((chunk, chunk_future)) + await asyncio.wait( + (fut, chunk_future), + return_when=asyncio.FIRST_COMPLETED, + ) + if fut.done(): + # The executor job failed + break + + queue.put_nowait(None) # terminate queue consumer + finally: + if fut is not None: + await fut + + def _move_and_cleanup() -> None: + shutil.move(target_temp_file, self.backup_dir / target_temp_file.name) + temp_dir_handler.cleanup() + + await self.hass.async_add_executor_job(_move_and_cleanup) + await self.load_backups() + async def async_create_backup(self, **kwargs: Any) -> Backup: """Generate a backup.""" if self.backing_up: diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 93ecb27bc97..76b1f76b55b 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -1,8 +1,11 @@ """Tests for the Backup integration.""" +import asyncio +from io import StringIO from unittest.mock import patch from aiohttp import web +import pytest from homeassistant.core import HomeAssistant @@ -49,12 +52,12 @@ async def test_downloading_backup_not_found( assert resp.status == 404 -async def test_non_admin( +async def test_downloading_as_non_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, ) -> None: - """Test downloading a backup file that does not exist.""" + """Test downloading a backup file when you are not an admin.""" hass_admin_user.groups = [] await setup_backup_integration(hass) @@ -62,3 +65,53 @@ async def test_non_admin( resp = await client.get("/api/backup/download/abc123") assert resp.status == 401 + + +async def test_uploading_a_backup_file( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test uploading a backup file.""" + await setup_backup_integration(hass) + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_receive_backup", + ) as async_receive_backup_mock: + resp = await client.post( + "/api/backup/upload", + data={"file": StringIO("test")}, + ) + assert resp.status == 201 + assert async_receive_backup_mock.called + + +@pytest.mark.parametrize( + ("error", "message"), + [ + (OSError("Boom!"), "Can't write backup file Boom!"), + (asyncio.CancelledError("Boom!"), ""), + ], +) +async def test_error_handling_uploading_a_backup_file( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + error: Exception, + message: str, +) -> None: + """Test error handling when uploading a backup file.""" + await setup_backup_integration(hass) + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_receive_backup", + side_effect=error, + ): + resp = await client.post( + "/api/backup/upload", + data={"file": StringIO("test")}, + ) + assert resp.status == 500 + assert await resp.text() == message diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index a4dba5c6936..a3f70267643 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -3,8 +3,10 @@ from __future__ import annotations from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch +import aiohttp +from multidict import CIMultiDict, CIMultiDictProxy import pytest from homeassistant.components.backup import BackupManager @@ -335,6 +337,40 @@ async def test_loading_platforms_when_running_async_post_backup_actions( assert "Loaded 1 platforms" in caplog.text +async def test_async_receive_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test receiving a backup file.""" + manager = BackupManager(hass) + + size = 2 * 2**16 + protocol = Mock(_reading_paused=False) + stream = aiohttp.StreamReader(protocol, 2**16) + stream.feed_data(b"0" * size + b"\r\n--:--") + stream.feed_eof() + + open_mock = mock_open() + + with patch("pathlib.Path.open", open_mock), patch("shutil.move") as mover_mock: + await manager.async_receive_backup( + contents=aiohttp.BodyPartReader( + b"--:", + CIMultiDictProxy( + CIMultiDict( + { + aiohttp.hdrs.CONTENT_DISPOSITION: "attachment; filename=abc123.tar" + } + ) + ), + stream, + ) + ) + assert open_mock.call_count == 1 + assert mover_mock.call_count == 1 + assert mover_mock.mock_calls[0].args[1].name == "abc123.tar" + + async def test_async_trigger_restore( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From 10a6f22addad3254abcd0804f8fa408a95a12ebb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Nov 2024 16:20:18 +0000 Subject: [PATCH 30/86] Fix deprecation version --- tests/components/vacuum/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 5b8e2c57a46..c7295f73170 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -92,7 +92,7 @@ def test_deprecated_constants_for_state( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum( - caplog, module, enum, constant_prefix, "2025.11" + caplog, module, enum, constant_prefix, "2025.12" ) From 167025a18c032998517e4a7762bf1a10997b49bb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:03:37 +0100 Subject: [PATCH 31/86] Simplify modern_forms config flow (#130441) * Simplify modern_forms config flow * Rename variable * Drop CONF_NAME --- .../components/modern_forms/config_flow.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index dee08736234..33e814efb51 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -9,11 +9,13 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a ModernForms config flow.""" @@ -55,17 +57,21 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, prepare: bool = False ) -> ConfigFlowResult: """Config flow handler for ModernForms.""" - source = self.context["source"] - # Request user input, unless we are preparing discovery flow if user_input is None: user_input = {} if not prepare: - if source == SOURCE_ZEROCONF: - return self._show_confirm_dialog() - return self._show_setup_form() + if self.source == SOURCE_ZEROCONF: + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": self.name}, + ) + return self.async_show_form( + step_id="user", + data_schema=USER_SCHEMA, + ) - if source == SOURCE_ZEROCONF: + if self.source == SOURCE_ZEROCONF: user_input[CONF_HOST] = self.host user_input[CONF_MAC] = self.mac @@ -75,18 +81,21 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): try: device = await device.update() except ModernFormsConnectionError: - if source == SOURCE_ZEROCONF: + if self.source == SOURCE_ZEROCONF: return self.async_abort(reason="cannot_connect") - return self._show_setup_form({"base": "cannot_connect"}) + return self.async_show_form( + step_id="user", + data_schema=USER_SCHEMA, + errors={"base": "cannot_connect"}, + ) user_input[CONF_MAC] = device.info.mac_address - user_input[CONF_NAME] = device.info.device_name # Check if already configured await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) title = device.info.device_name - if source == SOURCE_ZEROCONF: + if self.source == SOURCE_ZEROCONF: title = self.name if prepare: @@ -96,19 +105,3 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) - - def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), - errors=errors or {}, - ) - - def _show_confirm_dialog(self, errors: dict | None = None) -> ConfigFlowResult: - """Show the confirm dialog to the user.""" - return self.async_show_form( - step_id="zeroconf_confirm", - description_placeholders={"name": self.name}, - errors=errors or {}, - ) From 285468d85f7911b55a0450981ddb669d50009ffc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Nov 2024 18:44:32 +0100 Subject: [PATCH 32/86] Fix translation in statistics (#130455) * Fix translation in statistics * Update homeassistant/components/statistics/strings.json --- homeassistant/components/statistics/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index a060c88da24..3e6fec9d986 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -23,10 +23,10 @@ "state_characteristic": { "description": "Read the documention for further details on available options and how to use them.", "data": { - "state_characteristic": "State_characteristic" + "state_characteristic": "Statistic characteristic" }, "data_description": { - "state_characteristic": "The characteristic that should be used as the state of the statistics sensor." + "state_characteristic": "The statistic characteristic that should be used as the state of the sensor." } }, "options": { From 388473ecd7adaec1658caac9f05208ee9c319223 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Nov 2024 19:55:27 +0100 Subject: [PATCH 33/86] Add diagnostics to Nord Pool (#130461) --- .../components/nordpool/diagnostics.py | 16 + .../nordpool/snapshots/test_diagnostics.ambr | 283 ++++++++++++++++++ tests/components/nordpool/test_diagnostics.py | 23 ++ 3 files changed, 322 insertions(+) create mode 100644 homeassistant/components/nordpool/diagnostics.py create mode 100644 tests/components/nordpool/snapshots/test_diagnostics.ambr create mode 100644 tests/components/nordpool/test_diagnostics.py diff --git a/homeassistant/components/nordpool/diagnostics.py b/homeassistant/components/nordpool/diagnostics.py new file mode 100644 index 00000000000..3160c2bfa6d --- /dev/null +++ b/homeassistant/components/nordpool/diagnostics.py @@ -0,0 +1,16 @@ +"""Diagnostics support for Nord Pool.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant + +from . import NordPoolConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: NordPoolConfigEntry +) -> dict[str, Any]: + """Return diagnostics for Nord Pool config entry.""" + return {"raw": entry.runtime_data.data.raw} diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..dde2eca0022 --- /dev/null +++ b/tests/components/nordpool/snapshots/test_diagnostics.ambr @@ -0,0 +1,283 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'raw': dict({ + 'areaAverages': list([ + dict({ + 'areaCode': 'SE3', + 'price': 900.74, + }), + dict({ + 'areaCode': 'SE4', + 'price': 1166.12, + }), + ]), + 'areaStates': list([ + dict({ + 'areas': list([ + 'SE3', + 'SE4', + ]), + 'state': 'Final', + }), + ]), + 'blockPriceAggregates': list([ + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 422.87, + 'max': 1406.14, + 'min': 61.69, + }), + 'SE4': dict({ + 'average': 497.97, + 'max': 1648.25, + 'min': 65.19, + }), + }), + 'blockName': 'Off-peak 1', + 'deliveryEnd': '2024-11-05T07:00:00Z', + 'deliveryStart': '2024-11-04T23:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1315.97, + 'max': 2512.65, + 'min': 925.05, + }), + 'SE4': dict({ + 'average': 1735.59, + 'max': 3533.03, + 'min': 1081.72, + }), + }), + 'blockName': 'Peak', + 'deliveryEnd': '2024-11-05T19:00:00Z', + 'deliveryStart': '2024-11-05T07:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 610.79, + 'max': 835.53, + 'min': 289.14, + }), + 'SE4': dict({ + 'average': 793.98, + 'max': 1112.57, + 'min': 349.21, + }), + }), + 'blockName': 'Off-peak 2', + 'deliveryEnd': '2024-11-05T23:00:00Z', + 'deliveryStart': '2024-11-05T19:00:00Z', + }), + ]), + 'currency': 'SEK', + 'deliveryAreas': list([ + 'SE3', + 'SE4', + ]), + 'deliveryDateCET': '2024-11-05', + 'exchangeRate': 11.6402, + 'market': 'DayAhead', + 'multiAreaEntries': list([ + dict({ + 'deliveryEnd': '2024-11-05T00:00:00Z', + 'deliveryStart': '2024-11-04T23:00:00Z', + 'entryPerArea': dict({ + 'SE3': 250.73, + 'SE4': 283.79, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T01:00:00Z', + 'deliveryStart': '2024-11-05T00:00:00Z', + 'entryPerArea': dict({ + 'SE3': 76.36, + 'SE4': 81.36, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T02:00:00Z', + 'deliveryStart': '2024-11-05T01:00:00Z', + 'entryPerArea': dict({ + 'SE3': 73.92, + 'SE4': 79.15, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T03:00:00Z', + 'deliveryStart': '2024-11-05T02:00:00Z', + 'entryPerArea': dict({ + 'SE3': 61.69, + 'SE4': 65.19, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T04:00:00Z', + 'deliveryStart': '2024-11-05T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 64.6, + 'SE4': 68.44, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T05:00:00Z', + 'deliveryStart': '2024-11-05T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 453.27, + 'SE4': 516.71, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T06:00:00Z', + 'deliveryStart': '2024-11-05T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 996.28, + 'SE4': 1240.85, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T07:00:00Z', + 'deliveryStart': '2024-11-05T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1406.14, + 'SE4': 1648.25, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T08:00:00Z', + 'deliveryStart': '2024-11-05T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1346.54, + 'SE4': 1570.5, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T09:00:00Z', + 'deliveryStart': '2024-11-05T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1150.28, + 'SE4': 1345.37, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T10:00:00Z', + 'deliveryStart': '2024-11-05T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1031.32, + 'SE4': 1206.51, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T11:00:00Z', + 'deliveryStart': '2024-11-05T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 927.37, + 'SE4': 1085.8, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T12:00:00Z', + 'deliveryStart': '2024-11-05T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 925.05, + 'SE4': 1081.72, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T13:00:00Z', + 'deliveryStart': '2024-11-05T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 949.49, + 'SE4': 1130.38, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T14:00:00Z', + 'deliveryStart': '2024-11-05T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1042.03, + 'SE4': 1256.91, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T15:00:00Z', + 'deliveryStart': '2024-11-05T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1258.89, + 'SE4': 1765.82, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T16:00:00Z', + 'deliveryStart': '2024-11-05T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1816.45, + 'SE4': 2522.55, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T17:00:00Z', + 'deliveryStart': '2024-11-05T16:00:00Z', + 'entryPerArea': dict({ + 'SE3': 2512.65, + 'SE4': 3533.03, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T18:00:00Z', + 'deliveryStart': '2024-11-05T17:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1819.83, + 'SE4': 2524.06, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T19:00:00Z', + 'deliveryStart': '2024-11-05T18:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1011.77, + 'SE4': 1804.46, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T20:00:00Z', + 'deliveryStart': '2024-11-05T19:00:00Z', + 'entryPerArea': dict({ + 'SE3': 835.53, + 'SE4': 1112.57, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T21:00:00Z', + 'deliveryStart': '2024-11-05T20:00:00Z', + 'entryPerArea': dict({ + 'SE3': 796.19, + 'SE4': 1051.69, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T22:00:00Z', + 'deliveryStart': '2024-11-05T21:00:00Z', + 'entryPerArea': dict({ + 'SE3': 522.3, + 'SE4': 662.44, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T23:00:00Z', + 'deliveryStart': '2024-11-05T22:00:00Z', + 'entryPerArea': dict({ + 'SE3': 289.14, + 'SE4': 349.21, + }), + }), + ]), + 'updatedAt': '2024-11-04T12:15:03.9456464Z', + 'version': 3, + }), + }) +# --- diff --git a/tests/components/nordpool/test_diagnostics.py b/tests/components/nordpool/test_diagnostics.py new file mode 100644 index 00000000000..4639186ecf1 --- /dev/null +++ b/tests/components/nordpool/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Test Nord Pool diagnostics.""" + +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + load_int: ConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a config entry.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, load_int) == snapshot + ) From 6bfc0cbb0c1db6ade27290bf86cd29487af30ece Mon Sep 17 00:00:00 2001 From: Kelvin Dekker <143089625+KelvinDekker@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:33:52 +0100 Subject: [PATCH 34/86] Fix typo in file strings (#130465) --- homeassistant/components/file/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json index 60ebf451f78..8806c67cd96 100644 --- a/homeassistant/components/file/strings.json +++ b/homeassistant/components/file/strings.json @@ -18,7 +18,7 @@ }, "data_description": { "file_path": "The local file path to retrieve the sensor value from", - "value_template": "A template to render the the sensors value based on the file content", + "value_template": "A template to render the sensors value based on the file content", "unit_of_measurement": "Unit of measurement for the sensor" } }, From 5c52e865a0e95a83a94162e21424cd0be2d372c9 Mon Sep 17 00:00:00 2001 From: mrspouse <55619185+mrspouse@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:48:42 +0000 Subject: [PATCH 35/86] Correct spelling of BloodGlucoseConcentrationConverter (#130449) * Correct spelling of BloodGlucoseConcentrationConverter * Correct spelling of BloodGlucoseConcentrationConverter --- homeassistant/components/recorder/statistics.py | 6 +++--- homeassistant/components/recorder/websocket_api.py | 4 ++-- homeassistant/components/sensor/const.py | 4 ++-- homeassistant/util/unit_conversion.py | 2 +- tests/util/test_unit_conversion.py | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e5fbfe0e8c5..7243af9d4d5 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlugoseConcentrationConverter, + BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -130,8 +130,8 @@ QUERY_STATISTICS_SUMMARY_SUM = ( STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **{ - unit: BloodGlugoseConcentrationConverter - for unit in BloodGlugoseConcentrationConverter.VALID_UNITS + unit: BloodGlucoseConcentrationConverter + for unit in BloodGlucoseConcentrationConverter.VALID_UNITS }, **{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS}, **{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS}, diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 8b8d1cfb0c6..f4dce73fa47 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( - BloodGlugoseConcentrationConverter, + BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -56,7 +56,7 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10 UNIT_SCHEMA = vol.Schema( { vol.Optional("blood_glucose_concentration"): vol.In( - BloodGlugoseConcentrationConverter.VALID_UNITS + BloodGlucoseConcentrationConverter.VALID_UNITS ), vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS), vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index ee6167a5643..f4573f873a2 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -48,7 +48,7 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlugoseConcentrationConverter, + BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -501,7 +501,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlugoseConcentrationConverter, + SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter, SensorDeviceClass.CONDUCTIVITY: ConductivityConverter, SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.DATA_RATE: DataRateConverter, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 95d8fbc9df1..1bf3561e66a 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -174,7 +174,7 @@ class DistanceConverter(BaseUnitConverter): } -class BloodGlugoseConcentrationConverter(BaseUnitConverter): +class BloodGlucoseConcentrationConverter(BaseUnitConverter): """Utility to convert blood glucose concentration values.""" UNIT_CLASS = "blood_glucose_concentration" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index a57cdde821f..609809a96e8 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -33,7 +33,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlugoseConcentrationConverter, + BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -61,7 +61,7 @@ INVALID_SYMBOL = "bob" _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) for converter in ( - BloodGlugoseConcentrationConverter, + BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -83,7 +83,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { # Dict containing all converters with a corresponding unit ratio. _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { - BloodGlugoseConcentrationConverter: ( + BloodGlucoseConcentrationConverter: ( UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, 18, @@ -138,7 +138,7 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo _CONVERTED_VALUE: dict[ type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] ] = { - BloodGlugoseConcentrationConverter: [ + BloodGlucoseConcentrationConverter: [ ( 90, UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, From 4ff8b8015cdb5450f26707230194049a0af682ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Nov 2024 22:07:26 -0600 Subject: [PATCH 36/86] Bump aiohttp to 3.11.0rc2 (#130484) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a40c8745877..956ea032fe7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.1.1 -aiohttp==3.11.0rc1 +aiohttp==3.11.0rc2 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index adc85c0f4f7..8e588ce0b0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.0rc1", + "aiohttp==3.11.0rc2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 53d6b13a4ab..ac7c00b8050 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.0rc1 +aiohttp==3.11.0rc2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From a9f468509b7660737c79337aa11f815b6a0744ff Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 13 Nov 2024 01:14:39 -0500 Subject: [PATCH 37/86] Bump zwave-js-server-python to 0.59.1 (#130468) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index e3f643486a0..3631bf1163b 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -10,7 +10,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.59.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.59.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 67c7c991146..b7a979050bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3087,7 +3087,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.59.0 +zwave-js-server-python==0.59.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 048f0ac7d76..ec6be67d4b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2461,7 +2461,7 @@ zeversolar==0.3.2 zha==0.0.37 # homeassistant.components.zwave_js -zwave-js-server-python==0.59.0 +zwave-js-server-python==0.59.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 8b505a2273aeab31dd89ac86ce2cbb1b78f99e74 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 13 Nov 2024 07:35:51 +0100 Subject: [PATCH 38/86] Bump reolink_aio to 0.11.0 (#130481) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 23a46c5e1c9..22fd625770f 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.10.4"] + "requirements": ["reolink-aio==0.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b7a979050bf..0009c93f673 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2553,7 +2553,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.10.4 +reolink-aio==0.11.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec6be67d4b4..7ad45aae832 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2044,7 +2044,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.10.4 +reolink-aio==0.11.0 # homeassistant.components.rflink rflink==0.0.66 From fdb773c9216be11a342ca8a4aa3dd9749e065622 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 07:55:13 +0100 Subject: [PATCH 39/86] Add title to water heater component (#130446) --- homeassistant/components/water_heater/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 741b277d84d..07e132a0b5b 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -1,4 +1,5 @@ { + "title": "Water heater", "device_automation": { "action_type": { "turn_on": "[%key:common::device_automation::action_type::turn_on%]", @@ -7,7 +8,7 @@ }, "entity_component": { "_": { - "name": "Water heater", + "name": "[%key:component::water_heater::title%]", "state": { "off": "[%key:common::state::off%]", "eco": "Eco", From 5cce369ce82a4ece9a2ec3888751974626eb16de Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 07:55:33 +0100 Subject: [PATCH 40/86] Bump aiowithings to 3.1.2 (#130469) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index a0a86be5da3..c24bdb743bf 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.1.1"] + "requirements": ["aiowithings==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0009c93f673..a5898c91708 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.1 +aiowithings==3.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ad45aae832..a7f382e0251 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.1 +aiowithings==3.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.5 From 827875473bb133451005d4987aa07edc2a984a36 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:54:37 +0100 Subject: [PATCH 41/86] Fix RecursionError in Husqvarna Automower coordinator (#123085) * reach maximum recursion depth exceeded in tests * second background task * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * test * modify test * tests * use correct exception * reset mock * use recursion_limit * remove unneeded ticks * test TimeoutException * set lower recursionlimit * remove not that important comment and move the other * test that we connect and listen successfully * Simulate hass shutting down * skip testing against the recursion limit * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * mock * Remove comment * Revert "mock" This reverts commit e8ddaea3d79ed1aceb696a055cc42ad08b4febca. * Move patch to decorator * Make execution of patched methods predictable * Parametrize test, make mocked start_listening block * Apply suggestions from code review --------- Co-authored-by: Martin Hjelmare Co-authored-by: Erik --- .../husqvarna_automower/coordinator.py | 30 ++++--- .../husqvarna_automower/conftest.py | 8 ++ .../husqvarna_automower/test_init.py | 81 +++++++++++++++---- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 458ff50dac9..c19f37a040d 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -8,6 +8,7 @@ from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, + TimeoutException, ) from aioautomower.model import MowerAttributes from aioautomower.session import AutomowerSession @@ -22,6 +23,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAX_WS_RECONNECT_TIME = 600 SCAN_INTERVAL = timedelta(minutes=8) +DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): @@ -40,8 +42,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib update_interval=SCAN_INTERVAL, ) self.api = api - self.ws_connected: bool = False + self.reconnect_time = DEFAULT_RECONNECT_TIME async def _async_update_data(self) -> dict[str, MowerAttributes]: """Subscribe for websocket and poll data from the API.""" @@ -66,24 +68,28 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib hass: HomeAssistant, entry: ConfigEntry, automower_client: AutomowerSession, - reconnect_time: int = 2, ) -> None: """Listen with the client.""" try: await automower_client.auth.websocket_connect() - reconnect_time = 2 + # Reset reconnect time after successful connection + self.reconnect_time = DEFAULT_RECONNECT_TIME await automower_client.start_listening() except HusqvarnaWSServerHandshakeError as err: _LOGGER.debug( - "Failed to connect to websocket. Trying to reconnect: %s", err + "Failed to connect to websocket. Trying to reconnect: %s", + err, + ) + except TimeoutException as err: + _LOGGER.debug( + "Failed to listen to websocket. Trying to reconnect: %s", + err, ) - if not hass.is_stopping: - await asyncio.sleep(reconnect_time) - reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME) - await self.client_listen( - hass=hass, - entry=entry, - automower_client=automower_client, - reconnect_time=reconnect_time, + await asyncio.sleep(self.reconnect_time) + self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME) + entry.async_create_background_task( + hass, + self.client_listen(hass, entry, automower_client), + "reconnect_task", ) diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 2814e1558d1..0202cec05b9 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Husqvarna Automower.""" +import asyncio from collections.abc import Generator import time from unittest.mock import AsyncMock, patch @@ -101,10 +102,17 @@ async def setup_credentials(hass: HomeAssistant) -> None: def mock_automower_client(values) -> Generator[AsyncMock]: """Mock a Husqvarna Automower client.""" + async def listen() -> None: + """Mock listen.""" + listen_block = asyncio.Event() + await listen_block.wait() + pytest.fail("Listen was not cancelled!") + mock = AsyncMock(spec=AutomowerSession) mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) mock.commands = AsyncMock(spec_set=_MowerCommands) mock.get_status.return_value = values + mock.start_listening = AsyncMock(side_effect=listen) with patch( "homeassistant.components.husqvarna_automower.AutomowerSession", diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index ca0c2a04af1..ae688571d2c 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -1,14 +1,16 @@ """Tests for init module.""" -from datetime import datetime, timedelta +from asyncio import Event +from datetime import datetime import http import time -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, + TimeoutException, ) from aioautomower.model import MowerAttributes, WorkArea from freezegun.api import FrozenDateTimeFactory @@ -127,28 +129,77 @@ async def test_update_failed( assert entry.state is entry_state +@patch( + "homeassistant.components.husqvarna_automower.coordinator.DEFAULT_RECONNECT_TIME", 0 +) +@pytest.mark.parametrize( + ("method_path", "exception", "error_msg"), + [ + ( + ["auth", "websocket_connect"], + HusqvarnaWSServerHandshakeError, + "Failed to connect to websocket.", + ), + ( + ["start_listening"], + TimeoutException, + "Failed to listen to websocket.", + ), + ], +) async def test_websocket_not_available( hass: HomeAssistant, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, + method_path: list[str], + exception: type[Exception], + error_msg: str, ) -> None: - """Test trying reload the websocket.""" - mock_automower_client.start_listening.side_effect = HusqvarnaWSServerHandshakeError( - "Boom" - ) + """Test trying to reload the websocket.""" + calls = [] + mock_called = Event() + mock_stall = Event() + + async def mock_function(): + mock_called.set() + await mock_stall.wait() + # Raise the first time the method is awaited + if not calls: + calls.append(None) + raise exception("Boom") + if mock_side_effect: + await mock_side_effect() + + # Find the method to mock + mock = mock_automower_client + for itm in method_path: + mock = getattr(mock, itm) + mock_side_effect = mock.side_effect + mock.side_effect = mock_function + + # Setup integration and verify log error message await setup_integration(hass, mock_config_entry) - assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text - assert mock_automower_client.auth.websocket_connect.call_count == 1 - assert mock_automower_client.start_listening.call_count == 1 - assert mock_config_entry.state is ConfigEntryState.LOADED - freezer.tick(timedelta(seconds=2)) - async_fire_time_changed(hass) + await mock_called.wait() + mock_called.clear() + # Allow the exception to be raised + mock_stall.set() + assert mock.call_count == 1 await hass.async_block_till_done() - assert mock_automower_client.auth.websocket_connect.call_count == 2 - assert mock_automower_client.start_listening.call_count == 2 - assert mock_config_entry.state is ConfigEntryState.LOADED + assert f"{error_msg} Trying to reconnect: Boom" in caplog.text + + # Simulate a successful connection + caplog.clear() + await mock_called.wait() + mock_called.clear() + await hass.async_block_till_done() + assert mock.call_count == 2 + assert "Trying to reconnect: Boom" not in caplog.text + + # Simulate hass shutting down + await hass.async_stop() + assert mock.call_count == 2 async def test_device_info( From 3092297979cd11c176f85bd1129a8f801577daae Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 13 Nov 2024 09:55:52 +0100 Subject: [PATCH 42/86] Bump go2rtc-client to 0.1.1 (#130498) --- homeassistant/components/go2rtc/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index ea9308e5e18..201b7168847 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/go2rtc", "integration_type": "system", "iot_class": "local_polling", - "requirements": ["go2rtc-client==0.1.0"], + "requirements": ["go2rtc-client==0.1.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 956ea032fe7..7a0e43b299e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ ciso8601==2.3.1 cryptography==43.0.1 dbus-fast==2.24.3 fnv-hash-fast==1.0.2 -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 habluetooth==3.6.0 hass-nabucasa==0.84.0 diff --git a/requirements_all.txt b/requirements_all.txt index a5898c91708..9a27f4d3b04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -993,7 +993,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7f382e0251..38704005179 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ gios==5.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 # homeassistant.components.goalzero goalzero==0.2.2 From 0ac00ef0920067d241265393eb89ddd11e9ce65c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Nov 2024 10:55:28 +0100 Subject: [PATCH 43/86] Fix legacy _attr_state handling in AlarmControlPanel (#130479) --- .../alarm_control_panel/__init__.py | 14 ++- .../alarm_control_panel/test_init.py | 93 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 2946fc64941..a9e433a3650 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -6,7 +6,7 @@ import asyncio from datetime import timedelta from functools import partial import logging -from typing import Any, Final, final +from typing import TYPE_CHECKING, Any, Final, final from propcache import cached_property import voluptuous as vol @@ -221,9 +221,15 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A @property def state(self) -> str | None: """Return the current state.""" - if (alarm_state := self.alarm_state) is None: - return None - return alarm_state + if (alarm_state := self.alarm_state) is not None: + return alarm_state + if self._attr_state is not None: + # Backwards compatibility for integrations that set state directly + # Should be removed in 2025.11 + if TYPE_CHECKING: + assert isinstance(self._attr_state, str) + return self._attr_state + return None @cached_property def alarm_state(self) -> AlarmControlPanelState | None: diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index 90b23f87ab1..89a2a2a2b1a 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -489,3 +489,96 @@ async def test_alarm_control_panel_log_deprecated_state_warning_using_attr_state ) # Test we only log once assert "Entities should implement the 'alarm_state' property and" not in caplog.text + + +async def test_alarm_control_panel_deprecated_state_does_not_break_state( + hass: HomeAssistant, + code_format: CodeFormat | None, + supported_features: AlarmControlPanelEntityFeature, + code_arm_required: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test using _attr_state attribute does not break state.""" + + 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, [ALARM_CONTROL_PANEL_DOMAIN] + ) + return True + + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + class MockLegacyAlarmControlPanel(MockAlarmControlPanel): + """Mocked alarm control entity.""" + + def __init__( + self, + supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature( + 0 + ), + code_format: CodeFormat | None = None, + code_arm_required: bool = True, + ) -> None: + """Initialize the alarm control.""" + self._attr_state = "armed_away" + super().__init__(supported_features, code_format, code_arm_required) + + def alarm_disarm(self, code: str | None = None) -> None: + """Mock alarm disarm calls.""" + self._attr_state = "disarmed" + + entity = MockLegacyAlarmControlPanel( + supported_features=supported_features, + code_format=code_format, + code_arm_required=code_arm_required, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test alarm control panel platform via config entry.""" + async_add_entities([entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + with patch.object( + MockLegacyAlarmControlPanel, + "__module__", + "tests.custom_components.test.alarm_control_panel", + ): + 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 + assert state.state == "armed_away" + + with patch.object( + MockLegacyAlarmControlPanel, + "__module__", + "tests.custom_components.test.alarm_control_panel", + ): + await help_test_async_alarm_control_panel_service( + hass, entity.entity_id, SERVICE_ALARM_DISARM + ) + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == "disarmed" From 2eaaadd736e73ca4b90611ed13297572d990bf63 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 13 Nov 2024 11:01:05 +0100 Subject: [PATCH 44/86] Add go2rtc recommended version (#130508) --- .pre-commit-config.yaml | 2 +- homeassistant/components/go2rtc/__init__.py | 31 ++++++++++-- homeassistant/components/go2rtc/const.py | 1 + homeassistant/components/go2rtc/strings.json | 8 +++ script/hassfest/docker.py | 5 +- tests/components/go2rtc/conftest.py | 6 ++- tests/components/go2rtc/test_init.py | 52 ++++++++++++++++++-- 7 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/go2rtc/strings.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 519674b9894..56fbabe8087 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,7 +90,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$ + files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$ - id: hassfest-mypy-config name: hassfest-mypy-config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index fc91ef5e546..f1f6e44abc1 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -4,6 +4,7 @@ import logging import shutil from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError +from awesomeversion import AwesomeVersion from go2rtc_client import Go2RtcRestClient from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.ws import ( @@ -32,13 +33,23 @@ from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery_flow +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + issue_registry as ir, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL +from .const import ( + CONF_DEBUG_UI, + DEBUG_UI_URL_MESSAGE, + DOMAIN, + HA_MANAGED_URL, + RECOMMENDED_VERSION, +) from .server import Server _LOGGER = logging.getLogger(__name__) @@ -147,7 +158,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Validate the server URL try: client = Go2RtcRestClient(async_get_clientsession(hass), url) - await client.validate_server_version() + version = await client.validate_server_version() + if version < AwesomeVersion(RECOMMENDED_VERSION): + ir.async_create_issue( + hass, + DOMAIN, + "recommended_version", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="recommended_version", + translation_placeholders={ + "recommended_version": RECOMMENDED_VERSION, + "current_version": str(version), + }, + ) except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index d33ae3e3897..3c1c84c42b5 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,3 +6,4 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" +RECOMMENDED_VERSION = "1.9.7" diff --git a/homeassistant/components/go2rtc/strings.json b/homeassistant/components/go2rtc/strings.json new file mode 100644 index 00000000000..e350c19af96 --- /dev/null +++ b/homeassistant/components/go2rtc/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "recommended_version": { + "title": "Outdated go2rtc server detected", + "description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`." + } + } +} diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 9d38d8f7128..137bbc7ff66 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from pathlib import Path from homeassistant import core +from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION as GO2RTC_VERSION from homeassistant.const import Platform from homeassistant.util import executor, thread from script.gen_requirements_all import gather_recursive_requirements @@ -112,8 +113,6 @@ LABEL "com.github.actions.icon"="terminal" LABEL "com.github.actions.color"="gray-dark" """ -_GO2RTC_VERSION = "1.9.7" - def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: package_versions: dict[str, str] = {} @@ -197,7 +196,7 @@ def _generate_files(config: Config) -> list[File]: DOCKERFILE_TEMPLATE.format( timeout=timeout, **package_versions, - go2rtc=_GO2RTC_VERSION, + go2rtc=GO2RTC_VERSION, ), config.root / "Dockerfile", ), diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index 42b363b2324..abb139b89bf 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -3,9 +3,11 @@ from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch +from awesomeversion import AwesomeVersion from go2rtc_client.rest import _StreamClient, _WebRTCClient import pytest +from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION from homeassistant.components.go2rtc.server import Server GO2RTC_PATH = "homeassistant.components.go2rtc" @@ -23,7 +25,9 @@ def rest_client() -> Generator[AsyncMock]: client = mock_client.return_value client.streams = streams = Mock(spec_set=_StreamClient) streams.list.return_value = {} - client.validate_server_version = AsyncMock() + client.validate_server_version = AsyncMock( + return_value=AwesomeVersion(RECOMMENDED_VERSION) + ) client.webrtc = Mock(spec_set=_WebRTCClient) yield client diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 9388110366e..0f1cac6942d 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -6,6 +6,7 @@ from typing import NamedTuple from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError +from awesomeversion import AwesomeVersion from go2rtc_client import Stream from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.models import Producer @@ -36,10 +37,12 @@ from homeassistant.components.go2rtc.const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, + RECOMMENDED_VERSION, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -199,6 +202,7 @@ async def init_test_integration( async def _test_setup_and_signaling( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, config: ConfigType, @@ -211,6 +215,7 @@ async def _test_setup_and_signaling( assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done(wait_background_tasks=True) + assert issue_registry.async_get_issue(DOMAIN, "recommended_version") is None config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].state == ConfigEntryState.LOADED @@ -306,6 +311,7 @@ async def _test_setup_and_signaling( @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) async def test_setup_go_binary( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: AsyncMock, @@ -324,7 +330,13 @@ async def test_setup_go_binary( server_start.assert_called_once() await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + hass, + issue_registry, + rest_client, + ws_client, + config, + after_setup, + init_test_integration, ) await hass.async_stop() @@ -340,8 +352,9 @@ async def test_setup_go_binary( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_go( +async def test_setup( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: Mock, @@ -359,7 +372,13 @@ async def test_setup_go( server.assert_not_called() await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + hass, + issue_registry, + rest_client, + ws_client, + config, + after_setup, + init_test_integration, ) mock_get_binary.assert_not_called() @@ -711,3 +730,30 @@ async def test_config_entry_remove(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert not await hass.config_entries.async_setup(config_entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + +@pytest.mark.parametrize("config", [{DOMAIN: {CONF_URL: "http://localhost:1984"}}]) +@pytest.mark.usefixtures("server") +async def test_setup_with_recommended_version_repair( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + rest_client: AsyncMock, + config: ConfigType, +) -> None: + """Test setup integration entry fails.""" + rest_client.validate_server_version.return_value = AwesomeVersion("1.9.5") + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify the issue is created + issue = issue_registry.async_get_issue(DOMAIN, "recommended_version") + assert issue + assert issue.is_fixable is False + assert issue.is_persistent is False + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.issue_id == "recommended_version" + assert issue.translation_key == "recommended_version" + assert issue.translation_placeholders == { + "recommended_version": RECOMMENDED_VERSION, + "current_version": "1.9.5", + } From a06e7e31b9fb7629fe654515eb85e6722eb19807 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:06:38 +0100 Subject: [PATCH 45/86] Bump github/codeql-action from 3.27.1 to 3.27.3 (#130489) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.1 to 3.27.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3.27.1...v3.27.3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2c80c32245c..48e37717232 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.27.1 + uses: github/codeql-action/init@v3.27.3 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.27.1 + uses: github/codeql-action/analyze@v3.27.3 with: category: "/language:python" From e90893e2bc25e4f1c08ad699b4b17d985ffba394 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 11:43:31 +0100 Subject: [PATCH 46/86] Fix Music Assistant manifest (#130515) --- homeassistant/components/music_assistant/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index 23401f30abc..65e6652407f 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -4,9 +4,8 @@ "after_dependencies": ["media_source", "media_player"], "codeowners": ["@music-assistant"], "config_flow": true, - "documentation": "https://music-assistant.io", + "documentation": "https://www.home-assistant.io/integrations/music_assistant", "iot_class": "local_push", - "issue_tracker": "https://github.com/music-assistant/hass-music-assistant/issues", "loggers": ["music_assistant"], "requirements": ["music-assistant-client==1.0.5"], "zeroconf": ["_mass._tcp.local."] From b270e4556c395af63b325d3a0681d12e4f904e0f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 12:16:07 +0100 Subject: [PATCH 47/86] Avoid core manifest to have an issue tracker (#130514) --- script/hassfest/manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 6d2f4087f59..4013c8a6c19 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -268,7 +268,6 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( ) ], vol.Required("documentation"): vol.All(vol.Url(), documentation_url), - vol.Optional("issue_tracker"): vol.Url(), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("requirements"): [str], vol.Optional("dependencies"): [str], @@ -304,6 +303,7 @@ def manifest_schema(value: dict[str, Any]) -> vol.Schema: CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend( { vol.Optional("version"): vol.All(str, verify_version), + vol.Optional("issue_tracker"): vol.Url(), vol.Optional("import_executor"): bool, } ) From b78453b85b524ff422774fff2b549ac7cde23f55 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 12:21:15 +0100 Subject: [PATCH 48/86] Bump aiowithings to 3.1.3 (#130504) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index c24bdb743bf..f9e8328ae53 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.1.2"] + "requirements": ["aiowithings==3.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a27f4d3b04..334d36f0840 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.2 +aiowithings==3.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38704005179..c8d4fb15883 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.2 +aiowithings==3.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.5 From ab11b8467808831a53318b8eb42cd2c1f7e3eb00 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:01:54 +0100 Subject: [PATCH 49/86] Improve type hints in fritzbox config flow (#130509) --- homeassistant/components/fritzbox/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 76754fc5082..ffec4a9ea29 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -43,10 +43,11 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + _name: str + def __init__(self) -> None: """Initialize flow.""" self._host: str | None = None - self._name: str | None = None self._password: str | None = None self._username: str | None = None @@ -158,7 +159,6 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): result = await self.async_try_connect() if result == RESULT_SUCCESS: - assert self._name is not None return self._get_entry(self._name) if result != RESULT_INVALID_AUTH: return self.async_abort(reason=result) From 8300afc00d434dc53e172e7b3f2270915593b3fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:45:52 +0100 Subject: [PATCH 50/86] Improve type hints in fritz config flow (#130511) * Improve type hints in fritz config flow * Improve coverage * Apply suggestions from code review Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/fritz/config_flow.py | 14 ++++++----- tests/components/fritz/test_config_flow.py | 24 +++++++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ec9ffdd7554..920ecda1c52 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -57,6 +57,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + _host: str + @staticmethod @callback def async_get_options_flow( @@ -67,7 +69,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize FRITZ!Box Tools flow.""" - self._host: str | None = None self._name: str = "" self._password: str = "" self._use_tls: bool = False @@ -112,7 +113,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" - assert self._host current_host = await self.hass.async_add_executor_job( socket.gethostbyname, self._host ) @@ -154,15 +154,17 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "") - self._host = ssdp_location.hostname + host = ssdp_location.hostname + if not host or ipaddress.ip_address(host).is_link_local: + return self.async_abort(reason="ignore_ip6_link_local") + + self._host = host self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] ) - if not self._host or ipaddress.ip_address(self._host).is_link_local: - return self.async_abort(reason="ignore_ip6_link_local") - + uuid: str | None if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index e3fae8c083e..84f1b240b88 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -10,6 +10,7 @@ from fritzconnection.core.exceptions import ( ) import pytest +from homeassistant.components import ssdp from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -22,7 +23,6 @@ from homeassistant.components.fritz.const import ( ERROR_UNKNOWN, FRITZ_AUTH_EXCEPTIONS, ) -from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -644,7 +644,7 @@ async def test_ssdp_already_in_progress_host( MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy() - del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN] + del MOCK_NO_UNIQUE_ID.upnp[ssdp.ATTR_UPNP_UDN] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) @@ -737,3 +737,23 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_OLD_DISCOVERY: False, CONF_CONSIDER_HOME: 37, } + + +async def test_ssdp_ipv6_link_local(hass: HomeAssistant) -> None: + """Test ignoring ipv6-link-local while ssdp discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="https://[fe80::1ff:fe23:4567:890a]:12345/test", + upnp={ + ssdp.ATTR_UPNP_FRIENDLY_NAME: "fake_name", + ssdp.ATTR_UPNP_UDN: "uuid:only-a-test", + }, + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "ignore_ip6_link_local" From f6bc5f050ec92cac140013b76e025d8ff94f24ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 13 Nov 2024 14:28:19 +0100 Subject: [PATCH 51/86] Bump millheater to 0.12.2 (#130454) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 16e7bf552ba..6316eb72096 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.8", "mill-local==0.3.0"] + "requirements": ["millheater==0.12.2", "mill-local==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 334d36f0840..e562f218f83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1373,7 +1373,7 @@ microBeesPy==0.3.2 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.11.8 +millheater==0.12.2 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8d4fb15883..d74f9f8ba95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1142,7 +1142,7 @@ microBeesPy==0.3.2 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.11.8 +millheater==0.12.2 # homeassistant.components.minio minio==7.1.12 From 72b976f8322ad867aafe15eaa103f58f71d06a56 Mon Sep 17 00:00:00 2001 From: dunnmj Date: Wed, 13 Nov 2024 13:29:04 +0000 Subject: [PATCH 52/86] Add Sky remote integration (#124507) Co-authored-by: Kyle Cooke Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + homeassistant/brands/sky.json | 5 + .../components/sky_remote/__init__.py | 39 ++++++ .../components/sky_remote/config_flow.py | 64 +++++++++ homeassistant/components/sky_remote/const.py | 6 + .../components/sky_remote/manifest.json | 10 ++ homeassistant/components/sky_remote/remote.py | 70 ++++++++++ .../components/sky_remote/strings.json | 21 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 21 ++- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sky_remote/__init__.py | 13 ++ tests/components/sky_remote/conftest.py | 47 +++++++ .../components/sky_remote/test_config_flow.py | 125 ++++++++++++++++++ tests/components/sky_remote/test_init.py | 59 +++++++++ tests/components/sky_remote/test_remote.py | 46 +++++++ 17 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 homeassistant/brands/sky.json create mode 100644 homeassistant/components/sky_remote/__init__.py create mode 100644 homeassistant/components/sky_remote/config_flow.py create mode 100644 homeassistant/components/sky_remote/const.py create mode 100644 homeassistant/components/sky_remote/manifest.json create mode 100644 homeassistant/components/sky_remote/remote.py create mode 100644 homeassistant/components/sky_remote/strings.json create mode 100644 tests/components/sky_remote/__init__.py create mode 100644 tests/components/sky_remote/conftest.py create mode 100644 tests/components/sky_remote/test_config_flow.py create mode 100644 tests/components/sky_remote/test_init.py create mode 100644 tests/components/sky_remote/test_remote.py diff --git a/CODEOWNERS b/CODEOWNERS index 022eda00123..76422734c92 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1344,6 +1344,8 @@ build.json @home-assistant/supervisor /tests/components/siren/ @home-assistant/core @raman325 /homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sky_hub/ @rogerselwyn +/homeassistant/components/sky_remote/ @dunnmj @saty9 +/tests/components/sky_remote/ @dunnmj @saty9 /homeassistant/components/skybell/ @tkdrob /tests/components/skybell/ @tkdrob /homeassistant/components/slack/ @tkdrob @fletcherau diff --git a/homeassistant/brands/sky.json b/homeassistant/brands/sky.json new file mode 100644 index 00000000000..3ab0cbbe5bd --- /dev/null +++ b/homeassistant/brands/sky.json @@ -0,0 +1,5 @@ +{ + "domain": "sky", + "name": "Sky", + "integrations": ["sky_hub", "sky_remote"] +} diff --git a/homeassistant/components/sky_remote/__init__.py b/homeassistant/components/sky_remote/__init__.py new file mode 100644 index 00000000000..4daad78c558 --- /dev/null +++ b/homeassistant/components/sky_remote/__init__.py @@ -0,0 +1,39 @@ +"""The Sky Remote Control integration.""" + +import logging + +from skyboxremote import RemoteControl, SkyBoxConnectionError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +PLATFORMS = [Platform.REMOTE] + +_LOGGER = logging.getLogger(__name__) + + +type SkyRemoteConfigEntry = ConfigEntry[RemoteControl] + + +async def async_setup_entry(hass: HomeAssistant, entry: SkyRemoteConfigEntry) -> bool: + """Set up Sky remote.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + + _LOGGER.debug("Setting up Host: %s, Port: %s", host, port) + remote = RemoteControl(host, port) + try: + await remote.check_connectable() + except SkyBoxConnectionError as e: + raise ConfigEntryNotReady from e + + entry.runtime_data = remote + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sky_remote/config_flow.py b/homeassistant/components/sky_remote/config_flow.py new file mode 100644 index 00000000000..a55dfb2a52b --- /dev/null +++ b/homeassistant/components/sky_remote/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow for sky_remote.""" + +import logging +from typing import Any + +from skyboxremote import RemoteControl, SkyBoxConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_PORT, DOMAIN, LEGACY_PORT + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + } +) + + +async def async_find_box_port(host: str) -> int: + """Find port box uses for communication.""" + logging.debug("Attempting to find port to connect to %s on", host) + remote = RemoteControl(host, DEFAULT_PORT) + try: + await remote.check_connectable() + except SkyBoxConnectionError: + # Try legacy port if the default one failed + remote = RemoteControl(host, LEGACY_PORT) + await remote.check_connectable() + return LEGACY_PORT + return DEFAULT_PORT + + +class SkyRemoteConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sky Remote.""" + + VERSION = 1 + MINOR_VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step.""" + + errors: dict[str, str] = {} + if user_input is not None: + logging.debug("user_input: %s", user_input) + self._async_abort_entries_match(user_input) + try: + port = await async_find_box_port(user_input[CONF_HOST]) + except SkyBoxConnectionError: + logging.exception("while finding port of skybox") + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_HOST], + data={**user_input, CONF_PORT: port}, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/sky_remote/const.py b/homeassistant/components/sky_remote/const.py new file mode 100644 index 00000000000..e67744a741b --- /dev/null +++ b/homeassistant/components/sky_remote/const.py @@ -0,0 +1,6 @@ +"""Constants.""" + +DOMAIN = "sky_remote" + +DEFAULT_PORT = 49160 +LEGACY_PORT = 5900 diff --git a/homeassistant/components/sky_remote/manifest.json b/homeassistant/components/sky_remote/manifest.json new file mode 100644 index 00000000000..b00ff309b10 --- /dev/null +++ b/homeassistant/components/sky_remote/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "sky_remote", + "name": "Sky Remote Control", + "codeowners": ["@dunnmj", "@saty9"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sky_remote", + "integration_type": "device", + "iot_class": "assumed_state", + "requirements": ["skyboxremote==0.0.6"] +} diff --git a/homeassistant/components/sky_remote/remote.py b/homeassistant/components/sky_remote/remote.py new file mode 100644 index 00000000000..05a464f73a6 --- /dev/null +++ b/homeassistant/components/sky_remote/remote.py @@ -0,0 +1,70 @@ +"""Home Assistant integration to control a sky box using the remote platform.""" + +from collections.abc import Iterable +import logging +from typing import Any + +from skyboxremote import VALID_KEYS, RemoteControl + +from homeassistant.components.remote import RemoteEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import SkyRemoteConfigEntry +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config: SkyRemoteConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Sky remote platform.""" + async_add_entities( + [SkyRemote(config.runtime_data, config.entry_id)], + True, + ) + + +class SkyRemote(RemoteEntity): + """Representation of a Sky Remote.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, remote: RemoteControl, unique_id: str) -> None: + """Initialize the Sky Remote.""" + self._remote = remote + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="SKY", + model="Sky Box", + name=remote.host, + ) + + def turn_on(self, activity: str | None = None, **kwargs: Any) -> None: + """Send the power on command.""" + self.send_command(["sky"]) + + def turn_off(self, activity: str | None = None, **kwargs: Any) -> None: + """Send the power command.""" + self.send_command(["power"]) + + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a list of commands to the device.""" + for cmd in command: + if cmd not in VALID_KEYS: + raise ServiceValidationError( + f"{cmd} is not in Valid Keys: {VALID_KEYS}" + ) + try: + self._remote.send_keys(command) + except ValueError as err: + _LOGGER.error("Invalid command: %s. Error: %s", command, err) + return + _LOGGER.debug("Successfully sent command %s", command) diff --git a/homeassistant/components/sky_remote/strings.json b/homeassistant/components/sky_remote/strings.json new file mode 100644 index 00000000000..af794490c43 --- /dev/null +++ b/homeassistant/components/sky_remote/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "title": "Add Sky Remote", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "Hostname or IP address of your Sky device" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cbd30b560ce..78e16126542 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -537,6 +537,7 @@ FLOWS = { "simplefin", "simplepush", "simplisafe", + "sky_remote", "skybell", "slack", "sleepiq", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a1fdb9478f3..33a7d02776f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5608,11 +5608,22 @@ "config_flow": false, "iot_class": "local_push" }, - "sky_hub": { - "name": "Sky Hub", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" + "sky": { + "name": "Sky", + "integrations": { + "sky_hub": { + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_polling", + "name": "Sky Hub" + }, + "sky_remote": { + "integration_type": "device", + "config_flow": true, + "iot_class": "assumed_state", + "name": "Sky Remote Control" + } + } }, "skybeacon": { "name": "Skybeacon", diff --git a/requirements_all.txt b/requirements_all.txt index e562f218f83..97416c7ea39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2673,6 +2673,9 @@ simplisafe-python==2024.01.0 # homeassistant.components.sisyphus sisyphus-control==3.1.4 +# homeassistant.components.sky_remote +skyboxremote==0.0.6 + # homeassistant.components.slack slackclient==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d74f9f8ba95..3ffc1547722 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2131,6 +2131,9 @@ simplepush==2.2.3 # homeassistant.components.simplisafe simplisafe-python==2024.01.0 +# homeassistant.components.sky_remote +skyboxremote==0.0.6 + # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/sky_remote/__init__.py b/tests/components/sky_remote/__init__.py new file mode 100644 index 00000000000..83d68330d5b --- /dev/null +++ b/tests/components/sky_remote/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Sky Remote component.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_mock_entry(hass: HomeAssistant, entry: MockConfigEntry): + """Initialize a mock config entry.""" + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() diff --git a/tests/components/sky_remote/conftest.py b/tests/components/sky_remote/conftest.py new file mode 100644 index 00000000000..d6c453d81f7 --- /dev/null +++ b/tests/components/sky_remote/conftest.py @@ -0,0 +1,47 @@ +"""Test mocks and fixtures.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.sky_remote.const import DEFAULT_PORT, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +SAMPLE_CONFIG = {CONF_HOST: "example.com", CONF_PORT: DEFAULT_PORT} + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry(domain=DOMAIN, data=SAMPLE_CONFIG) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Stub out setup function.""" + with patch( + "homeassistant.components.sky_remote.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_remote_control(request: pytest.FixtureRequest) -> Generator[MagicMock]: + """Mock skyboxremote library.""" + with ( + patch( + "homeassistant.components.sky_remote.RemoteControl" + ) as mock_remote_control, + patch( + "homeassistant.components.sky_remote.config_flow.RemoteControl", + mock_remote_control, + ), + ): + mock_remote_control._instance_mock = MagicMock(host="example.com") + mock_remote_control._instance_mock.check_connectable = AsyncMock(True) + mock_remote_control.return_value = mock_remote_control._instance_mock + yield mock_remote_control diff --git a/tests/components/sky_remote/test_config_flow.py b/tests/components/sky_remote/test_config_flow.py new file mode 100644 index 00000000000..aaeda20788c --- /dev/null +++ b/tests/components/sky_remote/test_config_flow.py @@ -0,0 +1,125 @@ +"""Test the Sky Remote config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest +from skyboxremote import LEGACY_PORT, SkyBoxConnectionError + +from homeassistant.components.sky_remote.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import SAMPLE_CONFIG + + +async def test_user_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_remote_control +) -> None: + """Test we can setup an entry.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == SAMPLE_CONFIG + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_device_exists_abort( + hass: HomeAssistant, mock_config_entry, mock_remote_control +) -> None: + """Test we abort flow if device already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: mock_config_entry.data[CONF_HOST]}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("mock_remote_control", [LEGACY_PORT], indirect=True) +async def test_user_flow_legacy_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_remote_control, +) -> None: + """Test we can setup an entry with a legacy port.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + async def mock_check_connectable(): + if mock_remote_control.call_args[0][1] == LEGACY_PORT: + return True + raise SkyBoxConnectionError("Wrong port") + + mock_remote_control._instance_mock.check_connectable = mock_check_connectable + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {**SAMPLE_CONFIG, CONF_PORT: LEGACY_PORT} + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("mock_remote_control", [6], indirect=True) +async def test_user_flow_unconnectable( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_remote_control, +) -> None: + """Test we can setup an entry.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + mock_remote_control._instance_mock.check_connectable = AsyncMock( + side_effect=SkyBoxConnectionError("Example") + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + assert len(mock_setup_entry.mock_calls) == 0 + + mock_remote_control._instance_mock.check_connectable = AsyncMock(True) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == SAMPLE_CONFIG + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sky_remote/test_init.py b/tests/components/sky_remote/test_init.py new file mode 100644 index 00000000000..fe316baa6bf --- /dev/null +++ b/tests/components/sky_remote/test_init.py @@ -0,0 +1,59 @@ +"""Tests for the Sky Remote component.""" + +from unittest.mock import AsyncMock + +from skyboxremote import SkyBoxConnectionError + +from homeassistant.components.sky_remote.const import DEFAULT_PORT, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_mock_entry + +from tests.common import MockConfigEntry + + +async def test_setup_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_remote_control, + device_registry: dr.DeviceRegistry, +) -> None: + """Test successful setup of entry.""" + await setup_mock_entry(hass, mock_config_entry) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + mock_remote_control.assert_called_once_with("example.com", DEFAULT_PORT) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert device_entry is not None + assert device_entry.name == "example.com" + + +async def test_setup_unconnectable_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_remote_control, +) -> None: + """Test unsuccessful setup of entry.""" + mock_remote_control._instance_mock.check_connectable = AsyncMock( + side_effect=SkyBoxConnectionError() + ) + + await setup_mock_entry(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_remote_control +) -> None: + """Test unload an entry.""" + await setup_mock_entry(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sky_remote/test_remote.py b/tests/components/sky_remote/test_remote.py new file mode 100644 index 00000000000..301375bc039 --- /dev/null +++ b/tests/components/sky_remote/test_remote.py @@ -0,0 +1,46 @@ +"""Test sky_remote remote.""" + +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import setup_mock_entry + +ENTITY_ID = "remote.example_com" + + +async def test_send_command( + hass: HomeAssistant, mock_config_entry, mock_remote_control +) -> None: + """Test "send_command" method.""" + await setup_mock_entry(hass, mock_config_entry) + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["sky"]}, + blocking=True, + ) + mock_remote_control._instance_mock.send_keys.assert_called_once_with(["sky"]) + + +async def test_send_invalid_command( + hass: HomeAssistant, mock_config_entry, mock_remote_control +) -> None: + """Test "send_command" method.""" + await setup_mock_entry(hass, mock_config_entry) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["apple"]}, + blocking=True, + ) + mock_remote_control._instance_mock.send_keys.assert_not_called() From ac4cb52dbbda03307a938a2c561a2afcbb2365a8 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:04:23 +0000 Subject: [PATCH 53/86] Bump ring-doorbell to 0.9.12 (#130419) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 63c47cb2979..e431c680081 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell==0.9.9"] + "requirements": ["ring-doorbell==0.9.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 97416c7ea39..3de766e93c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2562,7 +2562,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.9 +ring-doorbell==0.9.12 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ffc1547722..b492a6f7020 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2050,7 +2050,7 @@ reolink-aio==0.11.0 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.9 +ring-doorbell==0.9.12 # homeassistant.components.roku rokuecp==0.19.3 From 093b16c7235a0ee69d88ff102e2838a747a96692 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Nov 2024 16:16:49 +0100 Subject: [PATCH 54/86] Make WS command backup/generate send events (#130524) * Make WS command backup/generate send events * Update backup.create service --- homeassistant/components/backup/__init__.py | 4 +- homeassistant/components/backup/manager.py | 62 ++++++++++-- homeassistant/components/backup/websocket.py | 11 ++- tests/components/backup/conftest.py | 73 ++++++++++++++ .../backup/snapshots/test_websocket.ambr | 17 +++- tests/components/backup/test_manager.py | 99 ++++++++----------- tests/components/backup/test_websocket.py | 18 ++-- 7 files changed, 199 insertions(+), 85 deletions(-) create mode 100644 tests/components/backup/conftest.py diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 200cb4a3f65..907fda4c7f8 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -32,7 +32,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_handle_create_service(call: ServiceCall) -> None: """Service handler for creating backups.""" - await backup_manager.async_create_backup() + await backup_manager.async_create_backup(on_progress=None) + if backup_task := backup_manager.backup_task: + await backup_task hass.services.async_register(DOMAIN, "create", async_handle_create_service) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 4300f75eed0..ddc0a1eac3f 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -4,6 +4,7 @@ from __future__ import annotations import abc import asyncio +from collections.abc import Callable from dataclasses import asdict, dataclass import hashlib import io @@ -34,6 +35,13 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER BUF_SIZE = 2**20 * 4 # 4MB +@dataclass(slots=True) +class NewBackup: + """New backup class.""" + + slug: str + + @dataclass(slots=True) class Backup: """Backup class.""" @@ -49,6 +57,15 @@ class Backup: return {**asdict(self), "path": self.path.as_posix()} +@dataclass(slots=True) +class BackupProgress: + """Backup progress class.""" + + done: bool + stage: str | None + success: bool | None + + class BackupPlatformProtocol(Protocol): """Define the format that backup platforms can have.""" @@ -65,7 +82,7 @@ class BaseBackupManager(abc.ABC): def __init__(self, hass: HomeAssistant) -> None: """Initialize the backup manager.""" self.hass = hass - self.backing_up = False + self.backup_task: asyncio.Task | None = None self.backups: dict[str, Backup] = {} self.loaded_platforms = False self.platforms: dict[str, BackupPlatformProtocol] = {} @@ -133,7 +150,12 @@ class BaseBackupManager(abc.ABC): """Restore a backup.""" @abc.abstractmethod - async def async_create_backup(self, **kwargs: Any) -> Backup: + async def async_create_backup( + self, + *, + on_progress: Callable[[BackupProgress], None] | None, + **kwargs: Any, + ) -> NewBackup: """Generate a backup.""" @abc.abstractmethod @@ -292,17 +314,36 @@ class BackupManager(BaseBackupManager): await self.hass.async_add_executor_job(_move_and_cleanup) await self.load_backups() - async def async_create_backup(self, **kwargs: Any) -> Backup: + async def async_create_backup( + self, + *, + on_progress: Callable[[BackupProgress], None] | None, + **kwargs: Any, + ) -> NewBackup: """Generate a backup.""" - if self.backing_up: + if self.backup_task: raise HomeAssistantError("Backup already in progress") + backup_name = f"Core {HAVERSION}" + date_str = dt_util.now().isoformat() + slug = _generate_slug(date_str, backup_name) + self.backup_task = self.hass.async_create_task( + self._async_create_backup(backup_name, date_str, slug, on_progress), + name="backup_manager_create_backup", + eager_start=False, # To ensure the task is not started before we return + ) + return NewBackup(slug=slug) + async def _async_create_backup( + self, + backup_name: str, + date_str: str, + slug: str, + on_progress: Callable[[BackupProgress], None] | None, + ) -> Backup: + """Generate a backup.""" + success = False try: - self.backing_up = True await self.async_pre_backup_actions() - backup_name = f"Core {HAVERSION}" - date_str = dt_util.now().isoformat() - slug = _generate_slug(date_str, backup_name) backup_data = { "slug": slug, @@ -329,9 +370,12 @@ class BackupManager(BaseBackupManager): if self.loaded_backups: self.backups[slug] = backup LOGGER.debug("Generated new backup with slug %s", slug) + success = True return backup finally: - self.backing_up = False + if on_progress: + on_progress(BackupProgress(done=True, stage=None, success=success)) + self.backup_task = None await self.async_post_backup_actions() def _mkdir_and_generate_backup_contents( diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 3ac8a7ace3e..a7c61b7c66c 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -8,6 +8,7 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from .const import DATA_MANAGER, LOGGER +from .manager import BackupProgress @callback @@ -40,7 +41,7 @@ async def handle_info( msg["id"], { "backups": list(backups.values()), - "backing_up": manager.backing_up, + "backing_up": manager.backup_task is not None, }, ) @@ -113,7 +114,11 @@ async def handle_create( msg: dict[str, Any], ) -> None: """Generate a backup.""" - backup = await hass.data[DATA_MANAGER].async_create_backup() + + def on_progress(progress: BackupProgress) -> None: + connection.send_message(websocket_api.event_message(msg["id"], progress)) + + backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress) connection.send_result(msg["id"], backup) @@ -127,7 +132,6 @@ async def handle_backup_start( ) -> None: """Backup start notification.""" manager = hass.data[DATA_MANAGER] - manager.backing_up = True LOGGER.debug("Backup start notification") try: @@ -149,7 +153,6 @@ async def handle_backup_end( ) -> None: """Backup end notification.""" manager = hass.data[DATA_MANAGER] - manager.backing_up = False LOGGER.debug("Backup end notification") try: diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py new file mode 100644 index 00000000000..631c774e63c --- /dev/null +++ b/tests/components/backup/conftest.py @@ -0,0 +1,73 @@ +"""Test fixtures for the Backup integration.""" + +from __future__ import annotations + +from collections.abc import Generator +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from homeassistant.core import HomeAssistant + + +@pytest.fixture(name="mocked_json_bytes") +def mocked_json_bytes_fixture() -> Generator[Mock]: + """Mock json_bytes.""" + with patch( + "homeassistant.components.backup.manager.json_bytes", + return_value=b"{}", # Empty JSON + ) as mocked_json_bytes: + yield mocked_json_bytes + + +@pytest.fixture(name="mocked_tarfile") +def mocked_tarfile_fixture() -> Generator[Mock]: + """Mock tarfile.""" + with patch( + "homeassistant.components.backup.manager.SecureTarFile" + ) as mocked_tarfile: + yield mocked_tarfile + + +@pytest.fixture(name="mock_backup_generation") +def mock_backup_generation_fixture( + hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock +) -> Generator[None]: + """Mock backup generator.""" + + def _mock_iterdir(path: Path) -> list[Path]: + if not path.name.endswith("testing_config"): + return [] + return [ + Path("test.txt"), + Path(".DS_Store"), + Path(".storage"), + ] + + with ( + patch("pathlib.Path.iterdir", _mock_iterdir), + patch("pathlib.Path.stat", MagicMock(st_size=123)), + patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), + patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), + patch( + "pathlib.Path.exists", + lambda x: x != Path(hass.config.path("backups")), + ), + patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), + patch( + "pathlib.Path.mkdir", + MagicMock(), + ), + patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ), + ): + yield diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 096df37d704..42eb524e529 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -210,16 +210,23 @@ dict({ 'id': 1, 'result': dict({ - 'date': '1970-01-01T00:00:00.000Z', - 'name': 'Test', - 'path': 'abc123.tar', - 'size': 0.0, - 'slug': 'abc123', + 'slug': '27f5c632', }), 'success': True, 'type': 'result', }) # --- +# name: test_generate[without_hassio].1 + dict({ + 'event': dict({ + 'done': True, + 'stage': None, + 'success': True, + }), + 'id': 1, + 'type': 'event', + }) +# --- # name: test_info[with_hassio] dict({ 'error': dict({ diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index a3f70267643..9d24964aedf 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -2,7 +2,7 @@ from __future__ import annotations -from pathlib import Path +import asyncio from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch import aiohttp @@ -10,7 +10,10 @@ from multidict import CIMultiDict, CIMultiDictProxy import pytest from homeassistant.components.backup import BackupManager -from homeassistant.components.backup.manager import BackupPlatformProtocol +from homeassistant.components.backup.manager import ( + BackupPlatformProtocol, + BackupProgress, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -20,59 +23,30 @@ from .common import TEST_BACKUP from tests.common import MockPlatform, mock_platform -async def _mock_backup_generation(manager: BackupManager): +async def _mock_backup_generation( + manager: BackupManager, mocked_json_bytes: Mock, mocked_tarfile: Mock +) -> None: """Mock backup generator.""" - def _mock_iterdir(path: Path) -> list[Path]: - if not path.name.endswith("testing_config"): - return [] - return [ - Path("test.txt"), - Path(".DS_Store"), - Path(".storage"), - ] + progress: list[BackupProgress] = [] - with ( - patch( - "homeassistant.components.backup.manager.SecureTarFile" - ) as mocked_tarfile, - patch("pathlib.Path.iterdir", _mock_iterdir), - patch("pathlib.Path.stat", MagicMock(st_size=123)), - patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), - patch( - "pathlib.Path.is_dir", - lambda x: x.name == ".storage", - ), - patch( - "pathlib.Path.exists", - lambda x: x != manager.backup_dir, - ), - patch( - "pathlib.Path.is_symlink", - lambda _: False, - ), - patch( - "pathlib.Path.mkdir", - MagicMock(), - ), - patch( - "homeassistant.components.backup.manager.json_bytes", - return_value=b"{}", # Empty JSON - ) as mocked_json_bytes, - patch( - "homeassistant.components.backup.manager.HAVERSION", - "2025.1.0", - ), - ): - await manager.async_create_backup() + def on_progress(_progress: BackupProgress) -> None: + """Mock progress callback.""" + progress.append(_progress) - assert mocked_json_bytes.call_count == 1 - backup_json_dict = mocked_json_bytes.call_args[0][0] - assert isinstance(backup_json_dict, dict) - assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"} - assert manager.backup_dir.as_posix() in str( - mocked_tarfile.call_args_list[0][0][0] - ) + assert manager.backup_task is None + await manager.async_create_backup(on_progress=on_progress) + assert manager.backup_task is not None + assert progress == [] + + await manager.backup_task + assert progress == [BackupProgress(done=True, stage=None, success=True)] + + assert mocked_json_bytes.call_count == 1 + backup_json_dict = mocked_json_bytes.call_args[0][0] + assert isinstance(backup_json_dict, dict) + assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"} + assert manager.backup_dir.as_posix() in str(mocked_tarfile.call_args_list[0][0][0]) async def _setup_mock_domain( @@ -176,21 +150,26 @@ async def test_getting_backup_that_does_not_exist( async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None: """Test generate backup.""" + event = asyncio.Event() manager = BackupManager(hass) - manager.backing_up = True + manager.backup_task = hass.async_create_task(event.wait()) with pytest.raises(HomeAssistantError, match="Backup already in progress"): - await manager.async_create_backup() + await manager.async_create_backup(on_progress=None) + event.set() +@pytest.mark.usefixtures("mock_backup_generation") async def test_async_create_backup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + mocked_json_bytes: Mock, + mocked_tarfile: Mock, ) -> None: """Test generate backup.""" manager = BackupManager(hass) manager.loaded_backups = True - await _mock_backup_generation(manager) + await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) assert "Generated new backup with slug " in caplog.text assert "Creating backup directory" in caplog.text @@ -247,7 +226,9 @@ async def test_not_loading_bad_platforms( ) -async def test_exception_plaform_pre(hass: HomeAssistant) -> None: +async def test_exception_plaform_pre( + hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock +) -> None: """Test exception in pre step.""" manager = BackupManager(hass) manager.loaded_backups = True @@ -264,10 +245,12 @@ async def test_exception_plaform_pre(hass: HomeAssistant) -> None: ) with pytest.raises(HomeAssistantError): - await _mock_backup_generation(manager) + await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) -async def test_exception_plaform_post(hass: HomeAssistant) -> None: +async def test_exception_plaform_post( + hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock +) -> None: """Test exception in post step.""" manager = BackupManager(hass) manager.loaded_backups = True @@ -284,7 +267,7 @@ async def test_exception_plaform_post(hass: HomeAssistant) -> None: ) with pytest.raises(HomeAssistantError): - await _mock_backup_generation(manager) + await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) async def test_loading_platforms_when_running_async_pre_backup_actions( diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 125ba8adaad..3e031f172ae 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -115,29 +116,30 @@ async def test_remove( @pytest.mark.parametrize( - "with_hassio", + ("with_hassio", "number_of_messages"), [ - pytest.param(True, id="with_hassio"), - pytest.param(False, id="without_hassio"), + pytest.param(True, 1, id="with_hassio"), + pytest.param(False, 2, id="without_hassio"), ], ) +@pytest.mark.usefixtures("mock_backup_generation") async def test_generate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, with_hassio: bool, + number_of_messages: int, ) -> None: """Test generating a backup.""" await setup_backup_integration(hass, with_hassio=with_hassio) client = await hass_ws_client(hass) + freezer.move_to("2024-11-13 12:01:00+01:00") await hass.async_block_till_done() - with patch( - "homeassistant.components.backup.manager.BackupManager.async_create_backup", - return_value=TEST_BACKUP, - ): - await client.send_json_auto_id({"type": "backup/generate"}) + await client.send_json_auto_id({"type": "backup/generate"}) + for _ in range(number_of_messages): assert snapshot == await client.receive_json() From 5f68d405b2fa0f08959dcb38a33444c6c330ee94 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:26:27 +0100 Subject: [PATCH 55/86] Update huum to 0.7.12 (#130527) --- homeassistant/components/huum/__init__.py | 15 ++++----------- homeassistant/components/huum/climate.py | 12 +++++------- homeassistant/components/huum/config_flow.py | 7 ++----- homeassistant/components/huum/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/huum/conftest.py | 6 ------ 7 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 tests/components/huum/conftest.py diff --git a/homeassistant/components/huum/__init__.py b/homeassistant/components/huum/__init__.py index c533ca34ef3..75faf1923df 100644 --- a/homeassistant/components/huum/__init__.py +++ b/homeassistant/components/huum/__init__.py @@ -3,30 +3,23 @@ from __future__ import annotations import logging -import sys + +from huum.exceptions import Forbidden, NotAuthenticated +from huum.huum import Huum from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, PLATFORMS -if sys.version_info < (3, 13): - from huum.exceptions import Forbidden, NotAuthenticated - from huum.huum import Huum - _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huum from a config entry.""" - if sys.version_info >= (3, 13): - raise HomeAssistantError( - "Huum is not supported on Python 3.13. Please use Python 3.12." - ) - username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/huum/climate.py b/homeassistant/components/huum/climate.py index b659e33038a..df740aea3d1 100644 --- a/homeassistant/components/huum/climate.py +++ b/homeassistant/components/huum/climate.py @@ -3,9 +3,13 @@ from __future__ import annotations import logging -import sys from typing import Any +from huum.const import SaunaStatus +from huum.exceptions import SafetyException +from huum.huum import Huum +from huum.schemas import HuumStatusResponse + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -20,12 +24,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -if sys.version_info < (3, 13): - from huum.const import SaunaStatus - from huum.exceptions import SafetyException - from huum.huum import Huum - from huum.schemas import HuumStatusResponse - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index 10c31378184..6a5fd96b99d 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -3,9 +3,10 @@ from __future__ import annotations import logging -import sys from typing import Any +from huum.exceptions import Forbidden, NotAuthenticated +from huum.huum import Huum import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -14,10 +15,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -if sys.version_info < (3, 13): - from huum.exceptions import Forbidden, NotAuthenticated - from huum.huum import Huum - _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/huum/manifest.json b/homeassistant/components/huum/manifest.json index 025d1b97f21..38562e1a072 100644 --- a/homeassistant/components/huum/manifest.json +++ b/homeassistant/components/huum/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huum", "iot_class": "cloud_polling", - "requirements": ["huum==0.7.11;python_version<'3.13'"] + "requirements": ["huum==0.7.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3de766e93c7..00984b9a5a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1148,7 +1148,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.11;python_version<'3.13' +huum==0.7.12 # homeassistant.components.hyperion hyperion-py==0.7.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b492a6f7020..ffda690bc33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.11;python_version<'3.13' +huum==0.7.12 # homeassistant.components.hyperion hyperion-py==0.7.5 diff --git a/tests/components/huum/conftest.py b/tests/components/huum/conftest.py deleted file mode 100644 index da66cc54b72..00000000000 --- a/tests/components/huum/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Skip test collection for Python 3.13.""" - -import sys - -if sys.version_info >= (3, 13): - collect_ignore_glob = ["test_*.py"] From 7e487f95631798895230173b9ba88e8cf36c25e6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Nov 2024 17:20:15 +0000 Subject: [PATCH 56/86] Mods --- homeassistant/components/vacuum/__init__.py | 14 +- tests/components/vacuum/test_init.py | 179 ++++++++++---------- 2 files changed, 104 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index f4548b96c6b..189b621d95e 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta from enum import IntFlag from functools import partial import logging -from typing import Any, final +from typing import TYPE_CHECKING, Any, final from propcache import cached_property import voluptuous as vol @@ -343,9 +343,15 @@ class StateVacuumEntity( @property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" - if (activity := self.activity) is None: - return None - return activity + 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 2025.12 + if TYPE_CHECKING: + assert isinstance(self._attr_state, str) + return self._attr_state + return None @cached_property def activity(self) -> VacuumActivity | None: diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index c7295f73170..5ff7720c1d5 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -24,23 +24,19 @@ from homeassistant.components.vacuum import ( VacuumActivity, VacuumEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import frame -from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MockVacuum, help_async_setup_entry_init, help_async_unload_entry from .common import async_start -from .conftest import TEST_DOMAIN from tests.common import ( MockConfigEntry, + MockEntity, MockModule, - MockPlatform, help_test_all, import_and_test_deprecated_constant_enum, mock_integration, - mock_platform, setup_test_component_platform, ) @@ -334,23 +330,6 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( ) -> None: """Test incorrectly using state property does log issue and raise repair.""" - 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, - ), - ) - class MockLegacyVacuum(MockVacuum): """Mocked vacuum entity.""" @@ -359,31 +338,23 @@ async def test_vacuum_log_deprecated_state_warning_using_state_prop( """Return the state of the entity.""" return VacuumActivity.CLEANING - entity = MockLegacyVacuum() - - 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), + entity = MockLegacyVacuum( + name="Testing", + entity_id="vacuum.test", ) + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) - with patch.object( - MockLegacyVacuum, - "__module__", - "tests.custom_components.test.vacuum", - ): - 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() + 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 @@ -402,23 +373,6 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( ) -> None: """Test incorrectly using _attr_state attribute does log issue and raise repair.""" - 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, - ), - ) - class MockLegacyVacuum(MockVacuum): """Mocked vacuum entity.""" @@ -426,31 +380,23 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( """Start cleaning.""" self._attr_state = VacuumActivity.CLEANING - entity = MockLegacyVacuum() - - 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), + entity = MockLegacyVacuum( + name="Testing", + entity_id="vacuum.test", ) + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) - with patch.object( - MockLegacyVacuum, - "__module__", - "tests.custom_components.test.vacuum", - ): - 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() + 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 @@ -483,3 +429,66 @@ async def test_vacuum_log_deprecated_state_warning_using_attr_state_attr( "should implement the 'activity' property and return its state using the VacuumActivity enum" not in caplog.text ) + + +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" + + with patch.object( + MockLegacyVacuum, + "__module__", + "tests.custom_components.test.alarm_control_panel", + ): + 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" From 7fd337d67f2ff1b1cfcbc61c36c1b7583a6cfcee Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Wed, 13 Nov 2024 10:42:26 -0700 Subject: [PATCH 57/86] fix translation in srp_energy (#130540) --- homeassistant/components/srp_energy/strings.json | 3 ++- tests/components/srp_energy/test_config_flow.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index 191d10a70dd..eca4f465435 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -17,7 +17,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "unknown": "Unexpected error" } }, "entity": { diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 149e08014ac..e3abb3c98df 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -100,10 +100,6 @@ async def test_form_invalid_auth( assert result["errors"] == {"base": "invalid_auth"} -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.srp_energy.config.abort.unknown"], -) async def test_form_unknown_error( hass: HomeAssistant, mock_srp_energy_config_flow: MagicMock, From 0a5a2de78e0677c1e146909b482b4299d7c4b172 Mon Sep 17 00:00:00 2001 From: Sheldon Ip <4224778+sheldonip@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:46:52 -0800 Subject: [PATCH 58/86] Fix translations in subaru (#130486) --- homeassistant/components/subaru/strings.json | 4 ++-- tests/components/subaru/test_config_flow.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 78625192e4a..00da729dccd 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -37,13 +37,13 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "incorrect_pin": "Incorrect PIN", "bad_pin_format": "PIN should be 4 digits", - "two_factor_request_failed": "Request for 2FA code failed, please try again", "bad_validation_code_format": "Validation code should be 6 digits", "incorrect_validation_code": "Incorrect validation code" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "two_factor_request_failed": "Request for 2FA code failed, please try again" } }, "options": { diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index d930aafbdfb..6abc544c92a 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -192,10 +192,6 @@ async def test_two_factor_request_success( assert len(mock_two_factor_request.mock_calls) == 1 -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.subaru.config.abort.two_factor_request_failed"], -) async def test_two_factor_request_fail( hass: HomeAssistant, two_factor_start_form ) -> None: From ed5560aec235ee6e31d6bcf836d00243ff36c035 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:28:53 +0100 Subject: [PATCH 59/86] Update base image to Python 3.13 and deprecated 3.12 (#130425) --- .github/workflows/builder.yml | 2 +- Dockerfile.dev | 2 +- build.yaml | 10 +++++----- homeassistant/const.py | 4 ++-- pyproject.toml | 1 + 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 7c08df39000..cc100c48fd8 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" diff --git a/Dockerfile.dev b/Dockerfile.dev index 48f582a1581..5a3f1a2ae64 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/python:1-3.12 +FROM mcr.microsoft.com/devcontainers/python:1-3.13 SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/build.yaml b/build.yaml index 13618740ab8..a8755bbbf5c 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/const.py b/homeassistant/const.py index 558e7ec2b0b..4082a076b94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -29,9 +29,9 @@ PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) -REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) +REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2025.2" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" diff --git a/pyproject.toml b/pyproject.toml index 8e588ce0b0e..a9b958e0805 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Home Automation", ] requires-python = ">=3.12.0" From c35ef6bda34aa8c01cae6ea6863cae24a5009fc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Nov 2024 12:32:14 -0600 Subject: [PATCH 60/86] Bump aiohttp to 3.11.0 (#130542) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a0e43b299e..abaf269103e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.1.1 -aiohttp==3.11.0rc2 +aiohttp==3.11.0 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index a9b958e0805..ebf22a93d7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.0rc2", + "aiohttp==3.11.0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index ac7c00b8050..b97c8dc57a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.0rc2 +aiohttp==3.11.0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 4002bc3c257507b82d08abcc836de767ba57c5d3 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:03:34 +0100 Subject: [PATCH 61/86] Downgrade devcontainer to Python 3.12 again (#130562) --- Dockerfile.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 5a3f1a2ae64..48f582a1581 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/python:1-3.13 +FROM mcr.microsoft.com/devcontainers/python:1-3.12 SHELL ["/bin/bash", "-o", "pipefail", "-c"] From 51c6ee97b19706eb56bb440a3b5155e3b34f3afd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 13 Nov 2024 15:50:08 -0600 Subject: [PATCH 62/86] Upgrade to hassil 2.0 (#130544) * Working on hassil 2.0 * Bump to hassil 2.0 * Update snapshots * Remove debug logging --- .../components/conversation/default_agent.py | 88 +++++-------------- homeassistant/components/conversation/http.py | 8 +- .../components/conversation/manifest.json | 2 +- .../components/conversation/trigger.py | 5 +- homeassistant/package_constraints.txt | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- script/hassfest/docker/Dockerfile | 2 +- .../snapshots/test_websocket.ambr | 4 +- .../conversation/snapshots/test_http.ambr | 4 +- .../conversation/test_default_agent.py | 28 +++--- tests/components/conversation/test_trace.py | 2 +- 12 files changed, 53 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a7110c35795..4838d19537a 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -16,11 +16,11 @@ from hassil.expression import Expression, ListReference, Sequence from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList from hassil.recognize import ( MISSING_ENTITY, - MatchEntity, RecognizeResult, - UnmatchedTextEntity, recognize_all, + recognize_best, ) +from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.util import merge_dict from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml @@ -499,6 +499,7 @@ class DefaultAgent(ConversationEntity): maybe_result: RecognizeResult | None = None best_num_matched_entities = 0 best_num_unmatched_entities = 0 + best_num_unmatched_ranges = 0 for result in recognize_all( user_input.text, lang_intents.intents, @@ -517,10 +518,14 @@ class DefaultAgent(ConversationEntity): num_matched_entities += 1 num_unmatched_entities = 0 + num_unmatched_ranges = 0 for unmatched_entity in result.unmatched_entities_list: if isinstance(unmatched_entity, UnmatchedTextEntity): if unmatched_entity.text != MISSING_ENTITY: num_unmatched_entities += 1 + elif isinstance(unmatched_entity, UnmatchedRangeEntity): + num_unmatched_ranges += 1 + num_unmatched_entities += 1 else: num_unmatched_entities += 1 @@ -532,15 +537,24 @@ class DefaultAgent(ConversationEntity): (num_matched_entities == best_num_matched_entities) and (num_unmatched_entities < best_num_unmatched_entities) ) + or ( + # Prefer unmatched ranges + (num_matched_entities == best_num_matched_entities) + and (num_unmatched_entities == best_num_unmatched_entities) + and (num_unmatched_ranges > best_num_unmatched_ranges) + ) or ( # More literal text matched (num_matched_entities == best_num_matched_entities) and (num_unmatched_entities == best_num_unmatched_entities) + and (num_unmatched_ranges == best_num_unmatched_ranges) and (result.text_chunks_matched > maybe_result.text_chunks_matched) ) or ( # Prefer match failures with entities (result.text_chunks_matched == maybe_result.text_chunks_matched) + and (num_unmatched_entities == best_num_unmatched_entities) + and (num_unmatched_ranges == best_num_unmatched_ranges) and ( ("name" in result.entities) or ("name" in result.unmatched_entities) @@ -550,6 +564,7 @@ class DefaultAgent(ConversationEntity): maybe_result = result best_num_matched_entities = num_matched_entities best_num_unmatched_entities = num_unmatched_entities + best_num_unmatched_ranges = num_unmatched_ranges return maybe_result @@ -562,76 +577,15 @@ class DefaultAgent(ConversationEntity): language: str, ) -> RecognizeResult | None: """Search intents for a strict match to user input.""" - custom_found = False - name_found = False - best_results: list[RecognizeResult] = [] - best_name_quality: int | None = None - best_text_chunks_matched: int | None = None - for result in recognize_all( + return recognize_best( user_input.text, lang_intents.intents, slot_lists=slot_lists, intent_context=intent_context, language=language, - ): - # Prioritize user intents - is_custom = ( - result.intent_metadata is not None - and result.intent_metadata.get(METADATA_CUSTOM_SENTENCE) - ) - - if custom_found and not is_custom: - continue - - if not custom_found and is_custom: - custom_found = True - # Clear builtin results - name_found = False - best_results = [] - best_name_quality = None - best_text_chunks_matched = None - - # Prioritize results with a "name" slot - name = result.entities.get("name") - is_name = name and not name.is_wildcard - - if name_found and not is_name: - continue - - if not name_found and is_name: - name_found = True - # Clear non-name results - best_results = [] - best_text_chunks_matched = None - - if is_name: - # Prioritize results with a better "name" slot - name_quality = len(cast(MatchEntity, name).value.split()) - if (best_name_quality is None) or (name_quality > best_name_quality): - best_name_quality = name_quality - # Clear worse name results - best_results = [] - best_text_chunks_matched = None - elif name_quality < best_name_quality: - continue - - # Prioritize results with more literal text - # This causes wildcards to match last. - if (best_text_chunks_matched is None) or ( - result.text_chunks_matched > best_text_chunks_matched - ): - best_results = [result] - best_text_chunks_matched = result.text_chunks_matched - elif result.text_chunks_matched == best_text_chunks_matched: - # Accumulate results with the same number of literal text matched. - # We will resolve the ambiguity below. - best_results.append(result) - - if best_results: - # Successful strict match - return best_results[0] - - return None + best_metadata_key=METADATA_CUSTOM_SENTENCE, + best_slot_name="name", + ) async def _build_speech( self, diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index df1ffc7f74f..5e5800ad6f1 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -6,12 +6,8 @@ from collections.abc import Iterable from typing import Any from aiohttp import web -from hassil.recognize import ( - MISSING_ENTITY, - RecognizeResult, - UnmatchedRangeEntity, - UnmatchedTextEntity, -) +from hassil.recognize import MISSING_ENTITY, RecognizeResult +from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity import voluptuous as vol from homeassistant.components import http, websocket_api diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 8b5c6ef173f..1676cdf8254 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"] + "requirements": ["hassil==2.0.1", "home-assistant-intents==2024.11.13"] } diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index ec7ecc76da0..a4f64ffbad9 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -4,7 +4,8 @@ from __future__ import annotations from typing import Any -from hassil.recognize import PUNCTUATION, RecognizeResult +from hassil.recognize import RecognizeResult +from hassil.util import PUNCTUATION_ALL import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -20,7 +21,7 @@ from .const import DATA_DEFAULT_ENTITY, DOMAIN def has_no_punctuation(value: list[str]) -> list[str]: """Validate result does not contain punctuation.""" for sentence in value: - if PUNCTUATION.search(sentence): + if PUNCTUATION_ALL.search(sentence): raise vol.Invalid("sentence should not contain punctuation") return value diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index abaf269103e..04e28fef58a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,10 +32,10 @@ go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 habluetooth==3.6.0 hass-nabucasa==0.84.0 -hassil==1.7.4 +hassil==2.0.1 home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241106.2 -home-assistant-intents==2024.11.6 +home-assistant-intents==2024.11.13 httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 00984b9a5a6..e9b5cb8129f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hass-nabucasa==0.84.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.7.4 +hassil==2.0.1 # homeassistant.components.jewish_calendar hdate==0.10.9 @@ -1130,7 +1130,7 @@ holidays==0.60 home-assistant-frontend==20241106.2 # homeassistant.components.conversation -home-assistant-intents==2024.11.6 +home-assistant-intents==2024.11.13 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffda690bc33..de08e2db395 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -928,7 +928,7 @@ habluetooth==3.6.0 hass-nabucasa==0.84.0 # homeassistant.components.conversation -hassil==1.7.4 +hassil==2.0.1 # homeassistant.components.jewish_calendar hdate==0.10.9 @@ -956,7 +956,7 @@ holidays==0.60 home-assistant-frontend==20241106.2 # homeassistant.components.conversation -home-assistant-intents==2024.11.6 +home-assistant-intents==2024.11.13 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 9bad1e8aecc..c921cf0e186 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 131444c17ac..b806c6faf23 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -697,7 +697,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any area called are', + 'speech': 'Sorry, I am not aware of any area called Are', }), }), }), @@ -741,7 +741,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any area called are', + 'speech': 'Sorry, I am not aware of any area called Are', }), }), }), diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 08aca43aba5..d9d859113f8 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -639,7 +639,7 @@ 'details': dict({ 'brightness': dict({ 'name': 'brightness', - 'text': '100%', + 'text': '100', 'value': 100, }), 'name': dict({ @@ -654,7 +654,7 @@ 'match': True, 'sentence_template': '[] brightness [to] ', 'slots': dict({ - 'brightness': '100%', + 'brightness': '100', 'name': 'test light', }), 'source': 'builtin', diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 9f54671d8a1..3c6b463670a 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -770,8 +770,8 @@ async def test_error_no_device_on_floor_exposed( ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "turn on test light on the ground floor", None, Context(), None @@ -838,8 +838,8 @@ async def test_error_no_domain(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "turn on the fans", None, Context(), None @@ -873,8 +873,8 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "turn on the fans", None, Context(), None @@ -1047,8 +1047,8 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "open the windows", None, Context(), None @@ -1096,8 +1096,8 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "open all the windows", None, Context(), None @@ -1207,8 +1207,8 @@ async def test_error_no_device_class_on_floor_exposed( ) with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[recognize_result], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=recognize_result, ): result = await conversation.async_converse( hass, "open ground floor windows", None, Context(), None @@ -1229,8 +1229,8 @@ async def test_error_no_device_class_on_floor_exposed( async def test_error_no_intent(hass: HomeAssistant) -> None: """Test response with an intent match failure.""" with patch( - "homeassistant.components.conversation.default_agent.recognize_all", - return_value=[], + "homeassistant.components.conversation.default_agent.recognize_best", + return_value=None, ): result = await conversation.async_converse( hass, "do something", None, Context(), None diff --git a/tests/components/conversation/test_trace.py b/tests/components/conversation/test_trace.py index 59cd10d2510..7c00b9a80b2 100644 --- a/tests/components/conversation/test_trace.py +++ b/tests/components/conversation/test_trace.py @@ -56,7 +56,7 @@ async def test_converation_trace( "intent_name": "HassListAddItem", "slots": { "name": "Shopping List", - "item": "apples ", + "item": "apples", }, } From 6a3b4a6a237382e640c87e0f3f644385e65abb6a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:49:39 +0100 Subject: [PATCH 63/86] Adjust minimum scapy version to 2.6.1 (#130565) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- tests/components/dhcp/conftest.py | 21 --------------------- 3 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 tests/components/dhcp/conftest.py diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04e28fef58a..5bc539beb86 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -181,8 +181,8 @@ chacha20poly1305-reuseable>=0.13.0 # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 pycountry>=23.12.11 -# scapy<2.5.0 will not work with python3.12 -scapy>=2.5.0 +# scapy==2.6.0 causes CI failures due to a race condition +scapy>=2.6.1 # tuf isn't updated to deal with breaking changes in securesystemslib==1.0. # Only tuf>=4 includes a constraint to <1.0. diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c5611069bf5..7d53741c661 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -214,8 +214,8 @@ chacha20poly1305-reuseable>=0.13.0 # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 pycountry>=23.12.11 -# scapy<2.5.0 will not work with python3.12 -scapy>=2.5.0 +# scapy==2.6.0 causes CI failures due to a race condition +scapy>=2.6.1 # tuf isn't updated to deal with breaking changes in securesystemslib==1.0. # Only tuf>=4 includes a constraint to <1.0. diff --git a/tests/components/dhcp/conftest.py b/tests/components/dhcp/conftest.py deleted file mode 100644 index b0fa3f573c5..00000000000 --- a/tests/components/dhcp/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Tests for the dhcp integration.""" - -import os -import pathlib - - -def pytest_sessionstart(session): - """Try to avoid flaky FileExistsError in CI. - - Called after the Session object has been created and - before performing collection and entering the run test loop. - - This is needed due to a race condition in scapy v2.6.0 - See https://github.com/secdev/scapy/pull/4558 - - Can be removed when scapy 2.6.1 is released. - """ - for sub_dir in (".cache", ".config"): - path = pathlib.Path(os.path.join(os.path.expanduser("~"), sub_dir)) - if not path.exists(): - path.mkdir(mode=0o700, exist_ok=True) From 4aad614497a3dc951ed7c616355b2e551137afef Mon Sep 17 00:00:00 2001 From: Tony <29752086+ms264556@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:43:59 +1300 Subject: [PATCH 64/86] Bump aioruckus to 0.42 (#130487) --- homeassistant/components/ruckus_unleashed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index 2066b65221e..8d56f3a5563 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioruckus"], - "requirements": ["aioruckus==0.41"] + "requirements": ["aioruckus==0.42"] } diff --git a/requirements_all.txt b/requirements_all.txt index e9b5cb8129f..a68fc1a828c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.41 +aioruckus==0.42 # homeassistant.components.russound_rio aiorussound==4.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de08e2db395..7501398f4d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -336,7 +336,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.41 +aioruckus==0.42 # homeassistant.components.russound_rio aiorussound==4.1.0 From 4200913d03489f67e8ca332dda0800c6d1303588 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Nov 2024 02:45:08 -0600 Subject: [PATCH 65/86] Fix non-thread-safe operation in powerview number (#130557) --- homeassistant/components/hunterdouglas_powerview/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index f893b04b2d1..fb8c9f76d79 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber): self.entity_description = description self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" - def set_native_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._attr_native_value = value self.entity_description.store_value_fn(self.coordinator, self._shade.id, value) From 2fda4c82de226f5d6e90bc3b81caa35c74756275 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 14 Nov 2024 18:46:24 +1000 Subject: [PATCH 66/86] Force login prompt in Tesla Fleet (#130576) --- homeassistant/components/tesla_fleet/oauth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_fleet/oauth.py b/homeassistant/components/tesla_fleet/oauth.py index 00976abf56f..8b43460436b 100644 --- a/homeassistant/components/tesla_fleet/oauth.py +++ b/homeassistant/components/tesla_fleet/oauth.py @@ -49,6 +49,7 @@ class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementati def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return { + "prompt": "login", "scope": " ".join(SCOPES), "code_challenge": self.code_challenge, # PKCE } @@ -83,4 +84,4 @@ class TeslaUserImplementation(AuthImplementation): @property def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" - return {"scope": " ".join(SCOPES)} + return {"prompt": "login", "scope": " ".join(SCOPES)} From 938b1eca2299130b28467632aa0b09aaa9c408c9 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 14 Nov 2024 03:52:28 -0500 Subject: [PATCH 67/86] Fix when the Roborock map is being provisioned (#130574) --- homeassistant/components/roborock/coordinator.py | 7 +++++-- homeassistant/components/roborock/select.py | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 20bc50f9855..fe592074f71 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -107,8 +106,12 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def _async_update_data(self) -> DeviceProp: """Update data via library.""" try: - await asyncio.gather(*(self._update_device_prop(), self.get_rooms())) + # Update device props and standard api information + await self._update_device_prop() + # Set the new map id from the updated device props self._set_current_map() + # Get the rooms for that map id. + await self.get_rooms() except RoborockException as ex: raise UpdateFailed(ex) from ex return self.roborock_device_info.props diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index 3dfe0e72a7b..73cb95d2d7c 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -135,6 +135,9 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): RoborockCommand.LOAD_MULTI_MAP, [map_id], ) + # Update the current map id manually so that nothing gets broken + # if another service hits the api. + self.coordinator.current_map = map_id # We need to wait after updating the map # so that other commands will be executed correctly. await asyncio.sleep(MAP_SLEEP) @@ -148,6 +151,9 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): @property def current_option(self) -> str | None: """Get the current status of the select entity from device_status.""" - if (current_map := self.coordinator.current_map) is not None: + if ( + (current_map := self.coordinator.current_map) is not None + and current_map in self.coordinator.maps + ): # 63 means it is searching for a map. return self.coordinator.maps[current_map].name return None From 2c1d1f577718dd08b0779e7ce786609c2c1df002 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:09:58 +0000 Subject: [PATCH 68/86] Do not trigger events for updated ring events (#130430) --- homeassistant/components/ring/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/event.py b/homeassistant/components/ring/event.py index e6d9d25542f..71a4bc8aea5 100644 --- a/homeassistant/components/ring/event.py +++ b/homeassistant/components/ring/event.py @@ -96,7 +96,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity) @callback def _handle_coordinator_update(self) -> None: - if alert := self._get_coordinator_alert(): + if (alert := self._get_coordinator_alert()) and not alert.is_update: self._async_handle_event(alert.kind) super()._handle_coordinator_update() From 58fd917cb763e876353437e9ab46304cd429872b Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 14 Nov 2024 04:11:44 -0500 Subject: [PATCH 69/86] Disable brightness from devices with no display in Cambridge Audio (#130369) --- homeassistant/components/cambridge_audio/manifest.json | 2 +- homeassistant/components/cambridge_audio/select.py | 7 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cambridge_audio/manifest.json b/homeassistant/components/cambridge_audio/manifest.json index edacd17f54d..c359ca14a21 100644 --- a/homeassistant/components/cambridge_audio/manifest.json +++ b/homeassistant/components/cambridge_audio/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aiostreammagic"], - "requirements": ["aiostreammagic==2.8.4"], + "requirements": ["aiostreammagic==2.8.5"], "zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."] } diff --git a/homeassistant/components/cambridge_audio/select.py b/homeassistant/components/cambridge_audio/select.py index ca6eebdec6b..c99abc853e5 100644 --- a/homeassistant/components/cambridge_audio/select.py +++ b/homeassistant/components/cambridge_audio/select.py @@ -51,8 +51,13 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = ( CambridgeAudioSelectEntityDescription( key="display_brightness", translation_key="display_brightness", - options=[x.value for x in DisplayBrightness], + options=[ + DisplayBrightness.BRIGHT.value, + DisplayBrightness.DIM.value, + DisplayBrightness.OFF.value, + ], entity_category=EntityCategory.CONFIG, + load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE, value_fn=lambda client: client.display.brightness, set_value_fn=lambda client, value: client.set_display_brightness( DisplayBrightness(value) diff --git a/requirements_all.txt b/requirements_all.txt index a68fc1a828c..32f111781da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -381,7 +381,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.4 +aiostreammagic==2.8.5 # homeassistant.components.switcher_kis aioswitcher==4.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7501398f4d3..237c70c8afb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -363,7 +363,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.4 +aiostreammagic==2.8.5 # homeassistant.components.switcher_kis aioswitcher==4.4.0 From 245fc246d85931c9697b9e1ba586fdde2e10325b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Nov 2024 04:13:29 -0500 Subject: [PATCH 70/86] Ensure ZHA setup works with container installs (#130470) --- homeassistant/components/zha/config_flow.py | 36 +++++++++-------- tests/components/zha/test_config_flow.py | 43 ++++++++++++++++----- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 1c7e0d105c4..f3f7f38772d 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt as dt_util @@ -104,25 +105,26 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: yellow_radio.description = "Yellow Zigbee module" yellow_radio.manufacturer = "Nabu Casa" - # Present the multi-PAN addon as a setup option, if it's available - multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( - hass - ) - - try: - addon_info = await multipan_manager.async_get_addon_info() - except (AddonError, KeyError): - addon_info = None - - if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: - addon_port = ListPortInfo( - device=silabs_multiprotocol_addon.get_zigbee_socket(), - skip_link_detection=True, + if is_hassio(hass): + # Present the multi-PAN addon as a setup option, if it's available + multipan_manager = ( + await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(hass) ) - addon_port.description = "Multiprotocol add-on" - addon_port.manufacturer = "Nabu Casa" - ports.append(addon_port) + try: + addon_info = await multipan_manager.async_get_addon_info() + except (AddonError, KeyError): + addon_info = None + + if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: + addon_port = ListPortInfo( + device=silabs_multiprotocol_addon.get_zigbee_socket(), + skip_link_detection=True, + ) + + addon_port.description = "Multiprotocol add-on" + addon_port.manufacturer = "Nabu Casa" + ports.append(addon_port) return ports diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 1382c5c2569..87ba46a4ced 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -21,7 +21,7 @@ import zigpy.types from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf -from homeassistant.components.hassio import AddonState +from homeassistant.components.hassio import AddonError, AddonState from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.const import ( @@ -1878,10 +1878,23 @@ async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: ) +async def test_config_flow_ports_no_hassio(hass: HomeAssistant) -> None: + """Test config flow serial port name when this is not a hassio install.""" + + with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=False), + patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), + ): + ports = await config_flow.list_serial_ports(hass) + + assert ports == [] + + async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> None: """Test config flow serial port name for multiprotocol add-on.""" with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), patch( "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" ) as async_get_addon_info, @@ -1889,16 +1902,28 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> ): async_get_addon_info.return_value.state = AddonState.RUNNING async_get_addon_info.return_value.hostname = "core-silabs-multiprotocol" + ports = await config_flow.list_serial_ports(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) + assert len(ports) == 1 + assert ports[0].description == "Multiprotocol add-on" + assert ports[0].manufacturer == "Nabu Casa" + assert ports[0].device == "socket://core-silabs-multiprotocol:9999" - assert ( - result["data_schema"].schema["path"].container[0] - == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" - ) + +async def test_config_flow_port_no_multiprotocol(hass: HomeAssistant) -> None: + """Test config flow serial port listing when addon info fails to load.""" + + with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), + patch( + "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info", + side_effect=AddonError, + ), + patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), + ): + ports = await config_flow.list_serial_ports(hass) + + assert ports == [] @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) From 301043ec387f581c8aedba8c7ac7475c53349048 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 10:27:45 +0100 Subject: [PATCH 71/86] Add require_webrtc_support decorator (#130519) --- homeassistant/components/camera/webrtc.py | 93 ++++++++++++----------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 0612c96e40c..d627a888169 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod import asyncio from collections.abc import Awaitable, Callable, Iterable from dataclasses import asdict, dataclass, field -from functools import cache, partial +from functools import cache, partial, wraps import logging from typing import TYPE_CHECKING, Any, Protocol @@ -205,6 +205,49 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None: ) +type WsCommandWithCamera = Callable[ + [websocket_api.ActiveConnection, dict[str, Any], Camera], + Awaitable[None], +] + + +def require_webrtc_support( + error_code: str, +) -> Callable[[WsCommandWithCamera], websocket_api.AsyncWebSocketCommandHandler]: + """Validate that the camera supports WebRTC.""" + + def decorate( + func: WsCommandWithCamera, + ) -> websocket_api.AsyncWebSocketCommandHandler: + """Decorate func.""" + + @wraps(func) + async def validate( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + """Validate that the camera supports WebRTC.""" + entity_id = msg["entity_id"] + camera = get_camera_from_entity_id(hass, entity_id) + if camera.frontend_stream_type != StreamType.WEB_RTC: + connection.send_error( + msg["id"], + error_code, + ( + "Camera does not support WebRTC," + f" frontend_stream_type={camera.frontend_stream_type}" + ), + ) + return + + await func(connection, msg, camera) + + return validate + + return decorate + + @websocket_api.websocket_command( { vol.Required("type"): "camera/webrtc/offer", @@ -213,8 +256,9 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None: } ) @websocket_api.async_response +@require_webrtc_support("webrtc_offer_failed") async def ws_webrtc_offer( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] + connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera ) -> None: """Handle the signal path for a WebRTC stream. @@ -226,20 +270,7 @@ async def ws_webrtc_offer( Async friendly. """ - entity_id = msg["entity_id"] offer = msg["offer"] - camera = get_camera_from_entity_id(hass, entity_id) - if camera.frontend_stream_type != StreamType.WEB_RTC: - connection.send_error( - msg["id"], - "webrtc_offer_failed", - ( - "Camera does not support WebRTC," - f" frontend_stream_type={camera.frontend_stream_type}" - ), - ) - return - session_id = ulid() connection.subscriptions[msg["id"]] = partial( camera.close_webrtc_session, session_id @@ -278,23 +309,11 @@ async def ws_webrtc_offer( } ) @websocket_api.async_response +@require_webrtc_support("webrtc_get_client_config_failed") async def ws_get_client_config( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] + connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera ) -> None: """Handle get WebRTC client config websocket command.""" - entity_id = msg["entity_id"] - camera = get_camera_from_entity_id(hass, entity_id) - if camera.frontend_stream_type != StreamType.WEB_RTC: - connection.send_error( - msg["id"], - "webrtc_get_client_config_failed", - ( - "Camera does not support WebRTC," - f" frontend_stream_type={camera.frontend_stream_type}" - ), - ) - return - config = camera.async_get_webrtc_client_configuration().to_frontend_dict() connection.send_result( msg["id"], @@ -311,23 +330,11 @@ async def ws_get_client_config( } ) @websocket_api.async_response +@require_webrtc_support("webrtc_candidate_failed") async def ws_candidate( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] + connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera ) -> None: """Handle WebRTC candidate websocket command.""" - entity_id = msg["entity_id"] - camera = get_camera_from_entity_id(hass, entity_id) - if camera.frontend_stream_type != StreamType.WEB_RTC: - connection.send_error( - msg["id"], - "webrtc_candidate_failed", - ( - "Camera does not support WebRTC," - f" frontend_stream_type={camera.frontend_stream_type}" - ), - ) - return - await camera.async_on_webrtc_candidate( msg["session_id"], RTCIceCandidate(msg["candidate"]) ) From 46cfe6aa32d30f9d8ecdb29742b3568d871d403f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 10:28:04 +0100 Subject: [PATCH 72/86] Refactor camera WebRTC tests (#130581) --- tests/components/camera/test_webrtc.py | 65 +++++++++++++------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index ba5cf35c52f..29fb9d61c4e 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -139,42 +139,46 @@ async def init_test_integration( return test_camera -@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_async_register_webrtc_provider( hass: HomeAssistant, ) -> None: """Test registering a WebRTC provider.""" - await async_setup_component(hass, "camera", {}) - camera = get_camera_from_entity_id(hass, "camera.demo_camera") - assert camera.frontend_stream_type is StreamType.HLS + assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} provider = SomeTestProvider() unregister = async_register_webrtc_provider(hass, provider) await hass.async_block_till_done() - assert camera.frontend_stream_type is StreamType.WEB_RTC + assert camera.camera_capabilities.frontend_stream_types == { + StreamType.HLS, + StreamType.WEB_RTC, + } # Mark stream as unsupported provider._is_supported = False # Manually refresh the provider await camera.async_refresh_providers() - assert camera.frontend_stream_type is StreamType.HLS + assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} # Mark stream as supported provider._is_supported = True # Manually refresh the provider await camera.async_refresh_providers() - assert camera.frontend_stream_type is StreamType.WEB_RTC + assert camera.camera_capabilities.frontend_stream_types == { + StreamType.HLS, + StreamType.WEB_RTC, + } unregister() await hass.async_block_till_done() - assert camera.frontend_stream_type is StreamType.HLS + assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} -@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_async_register_webrtc_provider_twice( hass: HomeAssistant, register_test_provider: SomeTestProvider, @@ -192,13 +196,11 @@ async def test_async_register_webrtc_provider_camera_not_loaded( async_register_webrtc_provider(hass, SomeTestProvider()) -@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_async_register_ice_server( hass: HomeAssistant, ) -> None: """Test registering an ICE server.""" - await async_setup_component(hass, "camera", {}) - # Clear any existing ICE servers hass.data[DATA_ICE_SERVERS].clear() @@ -216,7 +218,7 @@ async def test_async_register_ice_server( unregister = async_register_ice_servers(hass, get_ice_servers) assert not called - camera = get_camera_from_entity_id(hass, "camera.demo_camera") + camera = get_camera_from_entity_id(hass, "camera.async") config = camera.async_get_webrtc_client_configuration() assert config.configuration.ice_servers == [ @@ -277,7 +279,7 @@ async def test_async_register_ice_server( assert config.configuration.ice_servers == [] -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_ws_get_client_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -286,7 +288,7 @@ async def test_ws_get_client_config( client = await hass_ws_client(hass) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} ) msg = await client.receive_json() @@ -320,7 +322,7 @@ async def test_ws_get_client_config( async_register_ice_servers(hass, get_ice_server) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} ) msg = await client.receive_json() @@ -370,7 +372,7 @@ async def test_ws_get_client_config_sync_offer( } -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_ws_get_client_config_custom_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -384,7 +386,7 @@ async def test_ws_get_client_config_custom_config( client = await hass_ws_client(hass) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} ) msg = await client.receive_json() @@ -435,7 +437,7 @@ def mock_rtsp_to_webrtc_fixture(hass: HomeAssistant) -> Generator[Mock]: unsub() -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_websocket_webrtc_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -444,7 +446,7 @@ async def test_websocket_webrtc_offer( await client.send_json_auto_id( { "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", + "entity_id": "camera.async", "offer": WEBRTC_OFFER, } ) @@ -555,11 +557,11 @@ async def test_websocket_webrtc_offer_webrtc_provider( mock_async_close_session.assert_called_once_with(session_id) -@pytest.mark.usefixtures("mock_camera_webrtc") async def test_websocket_webrtc_offer_invalid_entity( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC with a camera entity that does not exist.""" + await async_setup_component(hass, "camera", {}) client = await hass_ws_client(hass) await client.send_json_auto_id( { @@ -578,7 +580,7 @@ async def test_websocket_webrtc_offer_invalid_entity( } -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_websocket_webrtc_offer_missing_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -605,7 +607,6 @@ async def test_websocket_webrtc_offer_missing_offer( (TimeoutError(), "Timeout handling WebRTC offer"), ], ) -@pytest.mark.usefixtures("mock_camera_webrtc_frontendtype_only") async def test_websocket_webrtc_offer_failure( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -949,7 +950,7 @@ async def test_rtsp_to_webrtc_offer_not_accepted( unsub() -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_ws_webrtc_candidate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -957,13 +958,13 @@ async def test_ws_webrtc_candidate( client = await hass_ws_client(hass) session_id = "session_id" candidate = "candidate" - with patch( - "homeassistant.components.camera.Camera.async_on_webrtc_candidate" + with patch.object( + get_camera_from_entity_id(hass, "camera.async"), "async_on_webrtc_candidate" ) as mock_on_webrtc_candidate: await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.demo_camera", + "entity_id": "camera.async", "session_id": session_id, "candidate": candidate, } @@ -976,7 +977,7 @@ async def test_ws_webrtc_candidate( ) -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_ws_webrtc_candidate_not_supported( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -985,7 +986,7 @@ async def test_ws_webrtc_candidate_not_supported( await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.demo_camera", + "entity_id": "camera.sync", "session_id": "session_id", "candidate": "candidate", } @@ -1028,11 +1029,11 @@ async def test_ws_webrtc_candidate_webrtc_provider( ) -@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_webrtc_candidate_invalid_entity( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test ws WebRTC candidate command with a camera entity that does not exist.""" + await async_setup_component(hass, "camera", {}) client = await hass_ws_client(hass) await client.send_json_auto_id( { @@ -1052,7 +1053,7 @@ async def test_ws_webrtc_candidate_invalid_entity( } -@pytest.mark.usefixtures("mock_camera_webrtc") +@pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_ws_webrtc_canidate_missing_candidate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1061,7 +1062,7 @@ async def test_ws_webrtc_canidate_missing_candidate( await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.demo_camera", + "entity_id": "camera.async", "session_id": "session_id", } ) From 93f79be2f4a83f3dd420a99a59076e2c61d7683f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 14 Nov 2024 10:35:03 +0100 Subject: [PATCH 73/86] Update uptime deviation for Vodafone Station (#130571) Update sensor.py --- homeassistant/components/vodafone_station/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index fb76253eb3d..307fcaf0ea8 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -22,7 +22,7 @@ from .const import _LOGGER, DOMAIN, LINE_TYPES from .coordinator import VodafoneStationRouter NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"] -UPTIME_DEVIATION = 45 +UPTIME_DEVIATION = 60 @dataclass(frozen=True, kw_only=True) From d0a58b68e8d35d2dea7bfdf14fd7a6a45b10fb99 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 14 Nov 2024 10:48:25 +0100 Subject: [PATCH 74/86] Bump reolink-aio to 0.11.1 (#130600) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 22fd625770f..7921bdb6ed5 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.11.0"] + "requirements": ["reolink-aio==0.11.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 32f111781da..9ad6a1199f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2553,7 +2553,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.0 +reolink-aio==0.11.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 237c70c8afb..68d1c393fc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2044,7 +2044,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.0 +reolink-aio==0.11.1 # homeassistant.components.rflink rflink==0.0.66 From 3201142fd8c3f84a7440c5ce4d76fd6597d8e9ed Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 11:01:26 +0100 Subject: [PATCH 75/86] Fix hassfest by adding go2rtc reqs (#130602) --- script/hassfest/docker.py | 2 ++ script/hassfest/docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 137bbc7ff66..0eb72b91c02 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -161,6 +161,8 @@ def _generate_hassfest_dockerimage( packages.update( gather_recursive_requirements(platform.value, already_checked_domains) ) + # Add go2rtc requirements as this file needs the go2rtc integration + packages.update(gather_recursive_requirements("go2rtc", already_checked_domains)) return File( _HASSFEST_TEMPLATE.format( diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index c921cf0e186..fe18c4dd486 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From a748897bd23b29be81b81487405c335ba217d7c2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:44:06 +0100 Subject: [PATCH 76/86] Update hassfest image to Python 3.13 (#130607) --- script/hassfest/docker.py | 2 +- script/hassfest/docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 0eb72b91c02..57d86bc4def 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -80,7 +80,7 @@ WORKDIR /config _HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.12-alpine +FROM python:3.13-alpine ENV \ UV_SYSTEM_PYTHON=true \ diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index fe18c4dd486..0fa0a1a89fa 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -1,7 +1,7 @@ # Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.12-alpine +FROM python:3.13-alpine ENV \ UV_SYSTEM_PYTHON=true \ From a949d18c30f86beabc21c73bae5e04d88da64bb8 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Thu, 14 Nov 2024 13:04:22 +0100 Subject: [PATCH 77/86] Bump eq3btsmart to 1.4.1 (#130426) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index bd3f14939ca..b30f806bf63 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "loggers": ["eq3btsmart"], "quality_scale": "silver", - "requirements": ["eq3btsmart==1.2.1", "bleak-esphome==1.1.0"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9ad6a1199f2..3b46bf19ae6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.2.1 +eq3btsmart==1.4.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68d1c393fc1..b27979b23f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -729,7 +729,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.2.1 +eq3btsmart==1.4.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 From eea782bbfe230168df52d8a30ceac94e463d2c98 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:28:38 +0100 Subject: [PATCH 78/86] Add acaia integration (#130059) Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + homeassistant/components/acaia/__init__.py | 29 +++ homeassistant/components/acaia/button.py | 61 +++++ homeassistant/components/acaia/config_flow.py | 149 +++++++++++ homeassistant/components/acaia/const.py | 4 + homeassistant/components/acaia/coordinator.py | 86 +++++++ homeassistant/components/acaia/entity.py | 40 +++ homeassistant/components/acaia/icons.json | 15 ++ homeassistant/components/acaia/manifest.json | 29 +++ homeassistant/components/acaia/strings.json | 38 +++ homeassistant/generated/bluetooth.py | 20 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/acaia/__init__.py | 14 + tests/components/acaia/conftest.py | 80 ++++++ .../acaia/snapshots/test_button.ambr | 139 ++++++++++ .../components/acaia/snapshots/test_init.ambr | 33 +++ tests/components/acaia/test_button.py | 83 ++++++ tests/components/acaia/test_config_flow.py | 242 ++++++++++++++++++ tests/components/acaia/test_init.py | 65 +++++ 22 files changed, 1142 insertions(+) create mode 100644 homeassistant/components/acaia/__init__.py create mode 100644 homeassistant/components/acaia/button.py create mode 100644 homeassistant/components/acaia/config_flow.py create mode 100644 homeassistant/components/acaia/const.py create mode 100644 homeassistant/components/acaia/coordinator.py create mode 100644 homeassistant/components/acaia/entity.py create mode 100644 homeassistant/components/acaia/icons.json create mode 100644 homeassistant/components/acaia/manifest.json create mode 100644 homeassistant/components/acaia/strings.json create mode 100644 tests/components/acaia/__init__.py create mode 100644 tests/components/acaia/conftest.py create mode 100644 tests/components/acaia/snapshots/test_button.ambr create mode 100644 tests/components/acaia/snapshots/test_init.ambr create mode 100644 tests/components/acaia/test_button.py create mode 100644 tests/components/acaia/test_config_flow.py create mode 100644 tests/components/acaia/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 76422734c92..8fd34a357c0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,6 +40,8 @@ build.json @home-assistant/supervisor # Integrations /homeassistant/components/abode/ @shred86 /tests/components/abode/ @shred86 +/homeassistant/components/acaia/ @zweckj +/tests/components/acaia/ @zweckj /homeassistant/components/accuweather/ @bieniu /tests/components/accuweather/ @bieniu /homeassistant/components/acmeda/ @atmurray diff --git a/homeassistant/components/acaia/__init__.py b/homeassistant/components/acaia/__init__.py new file mode 100644 index 00000000000..dfdb4cb935d --- /dev/null +++ b/homeassistant/components/acaia/__init__.py @@ -0,0 +1,29 @@ +"""Initialize the Acaia component.""" + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import AcaiaConfigEntry, AcaiaCoordinator + +PLATFORMS = [ + Platform.BUTTON, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool: + """Set up acaia as config entry.""" + + coordinator = AcaiaCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool: + """Unload a config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/acaia/button.py b/homeassistant/components/acaia/button.py new file mode 100644 index 00000000000..50671eecbba --- /dev/null +++ b/homeassistant/components/acaia/button.py @@ -0,0 +1,61 @@ +"""Button entities for Acaia scales.""" + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from aioacaia.acaiascale import AcaiaScale + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .coordinator import AcaiaConfigEntry +from .entity import AcaiaEntity + + +@dataclass(kw_only=True, frozen=True) +class AcaiaButtonEntityDescription(ButtonEntityDescription): + """Description for acaia button entities.""" + + press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]] + + +BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = ( + AcaiaButtonEntityDescription( + key="tare", + translation_key="tare", + press_fn=lambda scale: scale.tare(), + ), + AcaiaButtonEntityDescription( + key="reset_timer", + translation_key="reset_timer", + press_fn=lambda scale: scale.reset_timer(), + ), + AcaiaButtonEntityDescription( + key="start_stop", + translation_key="start_stop", + press_fn=lambda scale: scale.start_stop_timer(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AcaiaConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button entities and services.""" + + coordinator = entry.runtime_data + async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS) + + +class AcaiaButton(AcaiaEntity, ButtonEntity): + """Representation of an Acaia button.""" + + entity_description: AcaiaButtonEntityDescription + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.press_fn(self._scale) diff --git a/homeassistant/components/acaia/config_flow.py b/homeassistant/components/acaia/config_flow.py new file mode 100644 index 00000000000..36727059c8a --- /dev/null +++ b/homeassistant/components/acaia/config_flow.py @@ -0,0 +1,149 @@ +"""Config flow for Acaia integration.""" + +import logging +from typing import Any + +from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice +from aioacaia.helpers import is_new_scale +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS, CONF_NAME +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for acaia.""" + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered: dict[str, Any] = {} + self._discovered_devices: dict[str, str] = {} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + + errors: dict[str, str] = {} + + if user_input is not None: + mac = format_mac(user_input[CONF_ADDRESS]) + try: + is_new_style_scale = await is_new_scale(mac) + except AcaiaDeviceNotFound: + errors["base"] = "device_not_found" + except AcaiaError: + _LOGGER.exception("Error occurred while connecting to the scale") + errors["base"] = "unknown" + except AcaiaUnknownDevice: + return self.async_abort(reason="unsupported_device") + else: + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + if not errors: + return self.async_create_entry( + title=self._discovered_devices[user_input[CONF_ADDRESS]], + data={ + CONF_ADDRESS: mac, + CONF_IS_NEW_STYLE_SCALE: is_new_style_scale, + }, + ) + + for device in async_discovered_service_info(self.hass): + self._discovered_devices[device.address] = device.name + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + options = [ + SelectOptionDict( + value=device_mac, + label=f"{device_name} ({device_mac})", + ) + for device_mac, device_name in self._discovered_devices.items() + ] + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): SelectSelector( + SelectSelectorConfig( + options=options, + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ), + errors=errors, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle a discovered Bluetooth device.""" + + self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address) + self._discovered[CONF_NAME] = discovery_info.name + + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + try: + self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale( + discovery_info.address + ) + except AcaiaDeviceNotFound: + _LOGGER.debug("Device not found during discovery") + return self.async_abort(reason="device_not_found") + except AcaiaError: + _LOGGER.debug( + "Error occurred while connecting to the scale during discovery", + exc_info=True, + ) + return self.async_abort(reason="unknown") + except AcaiaUnknownDevice: + _LOGGER.debug("Unsupported device during discovery") + return self.async_abort(reason="unsupported_device") + + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle confirmation of Bluetooth discovery.""" + + if user_input is not None: + return self.async_create_entry( + title=self._discovered[CONF_NAME], + data={ + CONF_ADDRESS: self._discovered[CONF_ADDRESS], + CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE], + }, + ) + + self.context["title_placeholders"] = placeholders = { + CONF_NAME: self._discovered[CONF_NAME] + } + + self._set_confirm_only() + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders=placeholders, + ) diff --git a/homeassistant/components/acaia/const.py b/homeassistant/components/acaia/const.py new file mode 100644 index 00000000000..c603578763d --- /dev/null +++ b/homeassistant/components/acaia/const.py @@ -0,0 +1,4 @@ +"""Constants for component.""" + +DOMAIN = "acaia" +CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale" diff --git a/homeassistant/components/acaia/coordinator.py b/homeassistant/components/acaia/coordinator.py new file mode 100644 index 00000000000..bd915b42408 --- /dev/null +++ b/homeassistant/components/acaia/coordinator.py @@ -0,0 +1,86 @@ +"""Coordinator for Acaia integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioacaia.acaiascale import AcaiaScale +from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_IS_NEW_STYLE_SCALE + +SCAN_INTERVAL = timedelta(seconds=15) + +_LOGGER = logging.getLogger(__name__) + +type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator] + + +class AcaiaCoordinator(DataUpdateCoordinator[None]): + """Class to handle fetching data from the scale.""" + + config_entry: AcaiaConfigEntry + + def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name="acaia coordinator", + update_interval=SCAN_INTERVAL, + config_entry=entry, + ) + + self._scale = AcaiaScale( + address_or_ble_device=entry.data[CONF_ADDRESS], + name=entry.title, + is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE], + notify_callback=self.async_update_listeners, + ) + + @property + def scale(self) -> AcaiaScale: + """Return the scale object.""" + return self._scale + + async def _async_update_data(self) -> None: + """Fetch data.""" + + # scale is already connected, return + if self._scale.connected: + return + + # scale is not connected, try to connect + try: + await self._scale.connect(setup_tasks=False) + except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex: + _LOGGER.debug( + "Could not connect to scale: %s, Error: %s", + self.config_entry.data[CONF_ADDRESS], + ex, + ) + self._scale.device_disconnected_handler(notify=False) + return + + # connected, set up background tasks + if not self._scale.heartbeat_task or self._scale.heartbeat_task.done(): + self._scale.heartbeat_task = self.config_entry.async_create_background_task( + hass=self.hass, + target=self._scale.send_heartbeats(), + name="acaia_heartbeat_task", + ) + + if not self._scale.process_queue_task or self._scale.process_queue_task.done(): + self._scale.process_queue_task = ( + self.config_entry.async_create_background_task( + hass=self.hass, + target=self._scale.process_queue(), + name="acaia_process_queue_task", + ) + ) diff --git a/homeassistant/components/acaia/entity.py b/homeassistant/components/acaia/entity.py new file mode 100644 index 00000000000..8a2108d2687 --- /dev/null +++ b/homeassistant/components/acaia/entity.py @@ -0,0 +1,40 @@ +"""Base class for Acaia entities.""" + +from dataclasses import dataclass + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AcaiaCoordinator + + +@dataclass +class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]): + """Common elements for all entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AcaiaCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._scale = coordinator.scale + self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._scale.mac)}, + manufacturer="Acaia", + model=self._scale.model, + suggested_area="Kitchen", + ) + + @property + def available(self) -> bool: + """Returns whether entity is available.""" + return super().available and self._scale.connected diff --git a/homeassistant/components/acaia/icons.json b/homeassistant/components/acaia/icons.json new file mode 100644 index 00000000000..aeab07ee912 --- /dev/null +++ b/homeassistant/components/acaia/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "button": { + "tare": { + "default": "mdi:scale-balance" + }, + "reset_timer": { + "default": "mdi:timer-refresh" + }, + "start_stop": { + "default": "mdi:timer-play" + } + } + } +} diff --git a/homeassistant/components/acaia/manifest.json b/homeassistant/components/acaia/manifest.json new file mode 100644 index 00000000000..c907a70a38e --- /dev/null +++ b/homeassistant/components/acaia/manifest.json @@ -0,0 +1,29 @@ +{ + "domain": "acaia", + "name": "Acaia", + "bluetooth": [ + { + "manufacturer_id": 16962 + }, + { + "local_name": "ACAIA*" + }, + { + "local_name": "PYXIS-*" + }, + { + "local_name": "LUNAR-*" + }, + { + "local_name": "PROCHBT001" + } + ], + "codeowners": ["@zweckj"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/acaia", + "integration_type": "device", + "iot_class": "local_push", + "loggers": ["aioacaia"], + "requirements": ["aioacaia==0.1.6"] +} diff --git a/homeassistant/components/acaia/strings.json b/homeassistant/components/acaia/strings.json new file mode 100644 index 00000000000..f6a1aeb66fd --- /dev/null +++ b/homeassistant/components/acaia/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "flow_title": "{name}", + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "unsupported_device": "This device is not supported." + }, + "error": { + "device_not_found": "Device could not be found.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:common::config_flow::data::device%]" + } + } + } + }, + "entity": { + "button": { + "tare": { + "name": "Tare" + }, + "reset_timer": { + "name": "Reset timer" + }, + "start_stop": { + "name": "Start/stop timer" + } + } + } +} diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c4612898cb2..a105efc2685 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -8,6 +8,26 @@ from __future__ import annotations from typing import Final BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ + { + "domain": "acaia", + "manufacturer_id": 16962, + }, + { + "domain": "acaia", + "local_name": "ACAIA*", + }, + { + "domain": "acaia", + "local_name": "PYXIS-*", + }, + { + "domain": "acaia", + "local_name": "LUNAR-*", + }, + { + "domain": "acaia", + "local_name": "PROCHBT001", + }, { "domain": "airthings_ble", "manufacturer_id": 820, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 78e16126542..ffe61b915c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ FLOWS = { ], "integration": [ "abode", + "acaia", "accuweather", "acmeda", "adax", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 33a7d02776f..f007db87868 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -11,6 +11,12 @@ "config_flow": true, "iot_class": "cloud_push" }, + "acaia": { + "name": "Acaia", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "accuweather": { "name": "AccuWeather", "integration_type": "service", diff --git a/requirements_all.txt b/requirements_all.txt index 3b46bf19ae6..cdba146d251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,6 +172,9 @@ aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.10 +# homeassistant.components.acaia +aioacaia==0.1.6 + # homeassistant.components.airq aioairq==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b27979b23f2..39fb7f17d80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,6 +160,9 @@ aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.10 +# homeassistant.components.acaia +aioacaia==0.1.6 + # homeassistant.components.airq aioairq==0.3.2 diff --git a/tests/components/acaia/__init__.py b/tests/components/acaia/__init__.py new file mode 100644 index 00000000000..f4eaa39e615 --- /dev/null +++ b/tests/components/acaia/__init__.py @@ -0,0 +1,14 @@ +"""Common test tools for the acaia integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the acaia integration for testing.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/acaia/conftest.py b/tests/components/acaia/conftest.py new file mode 100644 index 00000000000..1dc6ff31051 --- /dev/null +++ b/tests/components/acaia/conftest.py @@ -0,0 +1,80 @@ +"""Common fixtures for the acaia tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from aioacaia.acaiascale import AcaiaDeviceState +from aioacaia.const import UnitMass as AcaiaUnitOfMass +import pytest + +from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.acaia.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_verify() -> Generator[AsyncMock]: + """Override is_new_scale check.""" + with patch( + "homeassistant.components.acaia.config_flow.is_new_scale", return_value=True + ) as mock_verify: + yield mock_verify + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="LUNAR-DDEEFF", + domain=DOMAIN, + version=1, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_IS_NEW_STYLE_SCALE: True, + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_scale: MagicMock +) -> None: + """Set up the acaia integration for testing.""" + await setup_integration(hass, mock_config_entry) + + +@pytest.fixture +def mock_scale() -> Generator[MagicMock]: + """Return a mocked acaia scale client.""" + with ( + patch( + "homeassistant.components.acaia.coordinator.AcaiaScale", + autospec=True, + ) as scale_mock, + ): + scale = scale_mock.return_value + scale.connected = True + scale.mac = "aa:bb:cc:dd:ee:ff" + scale.model = "Lunar" + scale.timer_running = True + scale.heartbeat_task = None + scale.process_queue_task = None + scale.device_state = AcaiaDeviceState( + battery_level=42, units=AcaiaUnitOfMass.GRAMS + ) + scale.weight = 123.45 + yield scale diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr new file mode 100644 index 00000000000..7e2624923af --- /dev/null +++ b/tests/components/acaia/snapshots/test_button.ambr @@ -0,0 +1,139 @@ +# serializer version: 1 +# name: test_buttons[entry_button_reset_timer] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.lunar_ddeeff_reset_timer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset timer', + 'platform': 'acaia', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_timer', + 'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[entry_button_start_stop_timer] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.lunar_ddeeff_start_stop_timer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start/stop timer', + 'platform': 'acaia', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start_stop', + 'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[entry_button_tare] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.lunar_ddeeff_tare', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tare', + 'platform': 'acaia', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tare', + 'unique_id': 'aa:bb:cc:dd:ee:ff_tare', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[state_button_reset_timer] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Reset timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_reset_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[state_button_start_stop_timer] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_start_stop_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[state_button_tare] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Tare', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_tare', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr new file mode 100644 index 00000000000..1cc3d8dbbc0 --- /dev/null +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device + DeviceRegistryEntrySnapshot({ + 'area_id': 'kitchen', + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'acaia', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Acaia', + 'model': 'Lunar', + 'model_id': None, + 'name': 'LUNAR-DDEEFF', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'Kitchen', + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py new file mode 100644 index 00000000000..62eb8b61b8a --- /dev/null +++ b/tests/components/acaia/test_button.py @@ -0,0 +1,83 @@ +"""Tests for the acaia buttons.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import async_fire_time_changed + +pytestmark = pytest.mark.usefixtures("init_integration") + + +BUTTONS = ( + "tare", + "reset_timer", + "start_stop_timer", +) + + +async def test_buttons( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the acaia buttons.""" + for button in BUTTONS: + state = hass.states.get(f"button.lunar_ddeeff_{button}") + assert state + assert state == snapshot(name=f"state_button_{button}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot(name=f"entry_button_{button}") + + +async def test_button_presses( + hass: HomeAssistant, + mock_scale: MagicMock, +) -> None: + """Test the acaia button presses.""" + + for button in BUTTONS: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: f"button.lunar_ddeeff_{button}", + }, + blocking=True, + ) + + function = getattr(mock_scale, button) + function.assert_called_once() + + +async def test_buttons_unavailable_on_disconnected_scale( + hass: HomeAssistant, + mock_scale: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the acaia buttons are unavailable when the scale is disconnected.""" + + for button in BUTTONS: + state = hass.states.get(f"button.lunar_ddeeff_{button}") + assert state + assert state.state == STATE_UNKNOWN + + mock_scale.connected = False + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for button in BUTTONS: + state = hass.states.get(f"button.lunar_ddeeff_{button}") + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/acaia/test_config_flow.py b/tests/components/acaia/test_config_flow.py new file mode 100644 index 00000000000..2bf4b1dbe8a --- /dev/null +++ b/tests/components/acaia/test_config_flow.py @@ -0,0 +1,242 @@ +"""Test the acaia config flow.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice +import pytest + +from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from tests.common import MockConfigEntry + +service_info = BluetoothServiceInfo( + name="LUNAR-DDEEFF", + address="aa:bb:cc:dd:ee:ff", + rssi=-63, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", +) + + +@pytest.fixture +def mock_discovered_service_info() -> Generator[AsyncMock]: + """Override getting Bluetooth service info.""" + with patch( + "homeassistant.components.acaia.config_flow.async_discovered_service_info", + return_value=[service_info], + ) as mock_discovered_service_info: + yield mock_discovered_service_info + + +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verify: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + user_input = { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + } + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "LUNAR-DDEEFF" + assert result2["data"] == { + **user_input, + CONF_IS_NEW_STYLE_SCALE: True, + } + + +async def test_bluetooth_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verify: AsyncMock, +) -> None: + """Test we can discover a device.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == service_info.name + assert result2["data"] == { + CONF_ADDRESS: service_info.address, + CONF_IS_NEW_STYLE_SCALE: True, + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (AcaiaDeviceNotFound("Error"), "device_not_found"), + (AcaiaError, "unknown"), + (AcaiaUnknownDevice, "unsupported_device"), + ], +) +async def test_bluetooth_discovery_errors( + hass: HomeAssistant, + mock_verify: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test abortions of Bluetooth discovery.""" + mock_verify.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == error + + +async def test_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_verify: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Ensure we can't add the same device twice.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_already_configured_bluetooth_discovery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure configure device is not discovered again.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (AcaiaDeviceNotFound("Error"), "device_not_found"), + (AcaiaError, "unknown"), + ], +) +async def test_recoverable_config_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verify: AsyncMock, + mock_discovered_service_info: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test recoverable errors.""" + mock_verify.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": error} + + # recover + mock_verify.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + assert result3["type"] is FlowResultType.CREATE_ENTRY + + +async def test_unsupported_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verify: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test flow aborts on unsupported device.""" + mock_verify.side_effect = AcaiaUnknownDevice + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unsupported_device" + + +async def test_no_bluetooth_devices( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test flow aborts on unsupported device.""" + mock_discovered_service_info.return_value = [] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/acaia/test_init.py b/tests/components/acaia/test_init.py new file mode 100644 index 00000000000..8ad988d3b9b --- /dev/null +++ b/tests/components/acaia/test_init.py @@ -0,0 +1,65 @@ +"""Test init of acaia integration.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.acaia.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry, async_fire_time_changed + +pytestmark = pytest.mark.usefixtures("init_integration") + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test loading and unloading the integration.""" + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "exception", [AcaiaError, AcaiaDeviceNotFound("Boom"), TimeoutError] +) +async def test_update_exception_leads_to_active_disconnect( + hass: HomeAssistant, + mock_scale: MagicMock, + freezer: FrozenDateTimeFactory, + exception: Exception, +) -> None: + """Test scale gets disconnected on exception.""" + + mock_scale.connect.side_effect = exception + mock_scale.connected = False + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_scale.device_disconnected_handler.assert_called_once() + + +async def test_device( + mock_scale: MagicMock, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Snapshot the device from registry.""" + + device = device_registry.async_get_device({(DOMAIN, mock_scale.mac)}) + assert device + assert device == snapshot From 3d84e35268e4024604f7a55acc15ef091788f228 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Thu, 14 Nov 2024 14:27:19 +0100 Subject: [PATCH 79/86] Move lcn non-config_entry related code to async_setup (#130603) * Move non-config_entry related code to async_setup * Remove action unload --- homeassistant/components/lcn/__init__.py | 32 +++++++++++------------- homeassistant/components/lcn/services.py | 8 ++++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 27f911822b5..eb26ef48e4e 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -20,7 +20,8 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType from .const import ( ADD_ENTITIES_CALLBACKS, @@ -41,15 +42,26 @@ from .helpers import ( register_lcn_address_devices, register_lcn_host_device, ) -from .services import SERVICES +from .services import register_services from .websocket import register_panel_and_ws_api _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the LCN component.""" + hass.data.setdefault(DOMAIN, {}) + + await register_services(hass) + await register_panel_and_ws_api(hass) + + return True + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a connection to PCHK host from a config entry.""" - hass.data.setdefault(DOMAIN, {}) if config_entry.entry_id in hass.data[DOMAIN]: return False @@ -109,15 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) lcn_connection.register_for_inputs(input_received) - # register service calls - for service_name, service in SERVICES: - if not hass.services.has_service(DOMAIN, service_name): - hass.services.async_register( - DOMAIN, service_name, service(hass).async_call_service, service.schema - ) - - await register_panel_and_ws_api(hass) - return True @@ -168,11 +171,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> host = hass.data[DOMAIN].pop(config_entry.entry_id) await host[CONNECTION].async_close() - # unregister service calls - if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload - for service_name, _ in SERVICES: - hass.services.async_remove(DOMAIN, service_name) - return unload_ok diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 611a7353bcd..92f5863c47e 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -429,3 +429,11 @@ SERVICES = ( (LcnService.DYN_TEXT, DynText), (LcnService.PCK, Pck), ) + + +async def register_services(hass: HomeAssistant) -> None: + """Register services for LCN.""" + for service_name, service in SERVICES: + hass.services.async_register( + DOMAIN, service_name, service(hass).async_call_service, service.schema + ) From 01332a542cbcc01ff8cfd4ae1bff6b8f4d4c01fe Mon Sep 17 00:00:00 2001 From: Thibaut Date: Thu, 14 Nov 2024 15:23:55 +0100 Subject: [PATCH 80/86] Removing myself from template codeowners (#130617) * Removing myself as codeowners * Fix --------- Co-authored-by: Joostlek --- CODEOWNERS | 4 ++-- homeassistant/components/template/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8fd34a357c0..e204463695e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1489,8 +1489,8 @@ build.json @home-assistant/supervisor /tests/components/tedee/ @patrickhilker @zweckj /homeassistant/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike -/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core -/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core +/homeassistant/components/template/ @PhracturedBlue @home-assistant/core +/tests/components/template/ @PhracturedBlue @home-assistant/core /homeassistant/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_wall_connector/ @einarhauks diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index 57188aebaa3..f1225f74f06 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -2,7 +2,7 @@ "domain": "template", "name": "Template", "after_dependencies": ["group"], - "codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"], + "codeowners": ["@PhracturedBlue", "@home-assistant/core"], "config_flow": true, "dependencies": ["blueprint"], "documentation": "https://www.home-assistant.io/integrations/template", From 61d0de3042dccf94332440e406ff27532e7e6163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 14 Nov 2024 15:27:10 +0100 Subject: [PATCH 81/86] Bump aioairzone to 0.9.6 (#130559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.9.6 Signed-off-by: Álvaro Fernández Rojas * Remove _async_migrator_mac_empty and improve tests Signed-off-by: Álvaro Fernández Rojas * Remove WebServer empty mac fixes as requested by @epenet Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 10fb20bb2ce..6bf374087a6 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.5"] + "requirements": ["aioairzone==0.9.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index cdba146d251..65ef5f1ebf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.5 +aioairzone==0.9.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39fb7f17d80..b61e65f3c68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.5 +aioairzone==0.9.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 0c44c632d47242cf5c9dacd7cf992e73114384c4 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Thu, 14 Nov 2024 15:38:38 +0100 Subject: [PATCH 82/86] Add number platform to eq3btsmart (#130429) --- .../components/eq3btsmart/__init__.py | 1 + homeassistant/components/eq3btsmart/const.py | 7 + .../components/eq3btsmart/icons.json | 17 ++ homeassistant/components/eq3btsmart/models.py | 3 - homeassistant/components/eq3btsmart/number.py | 158 ++++++++++++++++++ .../components/eq3btsmart/strings.json | 17 ++ 6 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/eq3btsmart/number.py diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py index 86c555ec151..84b27161edd 100644 --- a/homeassistant/components/eq3btsmart/__init__.py +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -21,6 +21,7 @@ from .models import Eq3Config, Eq3ConfigEntryData PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.NUMBER, Platform.SWITCH, ] diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py index 64bc1cf497c..78292940e60 100644 --- a/homeassistant/components/eq3btsmart/const.py +++ b/homeassistant/components/eq3btsmart/const.py @@ -24,6 +24,11 @@ ENTITY_KEY_WINDOW = "window" ENTITY_KEY_LOCK = "lock" ENTITY_KEY_BOOST = "boost" ENTITY_KEY_AWAY = "away" +ENTITY_KEY_COMFORT = "comfort" +ENTITY_KEY_ECO = "eco" +ENTITY_KEY_OFFSET = "offset" +ENTITY_KEY_WINDOW_OPEN_TEMPERATURE = "window_open_temperature" +ENTITY_KEY_WINDOW_OPEN_TIMEOUT = "window_open_timeout" GET_DEVICE_TIMEOUT = 5 # seconds @@ -77,3 +82,5 @@ DEFAULT_SCAN_INTERVAL = 10 # seconds SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected" SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected" + +EQ3BT_STEP = 0.5 diff --git a/homeassistant/components/eq3btsmart/icons.json b/homeassistant/components/eq3btsmart/icons.json index fb0862f14bc..e6eb7532f37 100644 --- a/homeassistant/components/eq3btsmart/icons.json +++ b/homeassistant/components/eq3btsmart/icons.json @@ -8,6 +8,23 @@ } } }, + "number": { + "comfort": { + "default": "mdi:sun-thermometer" + }, + "eco": { + "default": "mdi:snowflake-thermometer" + }, + "offset": { + "default": "mdi:thermometer-plus" + }, + "window_open_temperature": { + "default": "mdi:window-open-variant" + }, + "window_open_timeout": { + "default": "mdi:timer-refresh" + } + }, "switch": { "away": { "default": "mdi:home-account", diff --git a/homeassistant/components/eq3btsmart/models.py b/homeassistant/components/eq3btsmart/models.py index 8ea0955dbdd..858465effa8 100644 --- a/homeassistant/components/eq3btsmart/models.py +++ b/homeassistant/components/eq3btsmart/models.py @@ -2,7 +2,6 @@ from dataclasses import dataclass -from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP from eq3btsmart.thermostat import Thermostat from .const import ( @@ -23,8 +22,6 @@ class Eq3Config: target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR external_temp_sensor: str = "" scan_interval: int = DEFAULT_SCAN_INTERVAL - default_away_hours: float = DEFAULT_AWAY_HOURS - default_away_temperature: float = DEFAULT_AWAY_TEMP @dataclass(slots=True) diff --git a/homeassistant/components/eq3btsmart/number.py b/homeassistant/components/eq3btsmart/number.py new file mode 100644 index 00000000000..2e069180fa3 --- /dev/null +++ b/homeassistant/components/eq3btsmart/number.py @@ -0,0 +1,158 @@ +"""Platform for eq3 number entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from eq3btsmart import Thermostat +from eq3btsmart.const import ( + EQ3BT_MAX_OFFSET, + EQ3BT_MAX_TEMP, + EQ3BT_MIN_OFFSET, + EQ3BT_MIN_TEMP, +) +from eq3btsmart.models import Presets + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import Eq3ConfigEntry +from .const import ( + ENTITY_KEY_COMFORT, + ENTITY_KEY_ECO, + ENTITY_KEY_OFFSET, + ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, + ENTITY_KEY_WINDOW_OPEN_TIMEOUT, + EQ3BT_STEP, +) +from .entity import Eq3Entity + + +@dataclass(frozen=True, kw_only=True) +class Eq3NumberEntityDescription(NumberEntityDescription): + """Entity description for eq3 number entities.""" + + value_func: Callable[[Presets], float] + value_set_func: Callable[ + [Thermostat], + Callable[[float], Awaitable[None]], + ] + mode: NumberMode = NumberMode.BOX + entity_category: EntityCategory | None = EntityCategory.CONFIG + + +NUMBER_ENTITY_DESCRIPTIONS = [ + Eq3NumberEntityDescription( + key=ENTITY_KEY_COMFORT, + value_func=lambda presets: presets.comfort_temperature.value, + value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature, + translation_key=ENTITY_KEY_COMFORT, + native_min_value=EQ3BT_MIN_TEMP, + native_max_value=EQ3BT_MAX_TEMP, + native_step=EQ3BT_STEP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + Eq3NumberEntityDescription( + key=ENTITY_KEY_ECO, + value_func=lambda presets: presets.eco_temperature.value, + value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature, + translation_key=ENTITY_KEY_ECO, + native_min_value=EQ3BT_MIN_TEMP, + native_max_value=EQ3BT_MAX_TEMP, + native_step=EQ3BT_STEP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + Eq3NumberEntityDescription( + key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, + value_func=lambda presets: presets.window_open_temperature.value, + value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature, + translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, + native_min_value=EQ3BT_MIN_TEMP, + native_max_value=EQ3BT_MAX_TEMP, + native_step=EQ3BT_STEP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + Eq3NumberEntityDescription( + key=ENTITY_KEY_OFFSET, + value_func=lambda presets: presets.offset_temperature.value, + value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset, + translation_key=ENTITY_KEY_OFFSET, + native_min_value=EQ3BT_MIN_OFFSET, + native_max_value=EQ3BT_MAX_OFFSET, + native_step=EQ3BT_STEP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + Eq3NumberEntityDescription( + key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, + value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration, + value_func=lambda presets: presets.window_open_time.value.total_seconds() / 60, + translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, + native_min_value=0, + native_max_value=60, + native_step=5, + native_unit_of_measurement=UnitOfTime.MINUTES, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: Eq3ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the entry.""" + + async_add_entities( + Eq3NumberEntity(entry, entity_description) + for entity_description in NUMBER_ENTITY_DESCRIPTIONS + ) + + +class Eq3NumberEntity(Eq3Entity, NumberEntity): + """Base class for all eq3 number entities.""" + + entity_description: Eq3NumberEntityDescription + + def __init__( + self, entry: Eq3ConfigEntry, entity_description: Eq3NumberEntityDescription + ) -> None: + """Initialize the entity.""" + + super().__init__(entry, entity_description.key) + self.entity_description = entity_description + + @property + def native_value(self) -> float: + """Return the state of the entity.""" + + if TYPE_CHECKING: + assert self._thermostat.status is not None + assert self._thermostat.status.presets is not None + + return self.entity_description.value_func(self._thermostat.status.presets) + + async def async_set_native_value(self, value: float) -> None: + """Set the state of the entity.""" + + await self.entity_description.value_set_func(self._thermostat)(value) + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + + return ( + self._thermostat.status is not None + and self._thermostat.status.presets is not None + and self._attr_available + ) diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json index 03c3b21b964..acfd5082f45 100644 --- a/homeassistant/components/eq3btsmart/strings.json +++ b/homeassistant/components/eq3btsmart/strings.json @@ -25,6 +25,23 @@ "name": "Daylight saving time" } }, + "number": { + "comfort": { + "name": "Comfort temperature" + }, + "eco": { + "name": "Eco temperature" + }, + "offset": { + "name": "Offset temperature" + }, + "window_open_temperature": { + "name": "Window open temperature" + }, + "window_open_timeout": { + "name": "Window open timeout" + } + }, "switch": { "lock": { "name": "Lock" From 472414a8d6bd231ce9f5c661248a2fdfd97eabb1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:17:08 +0100 Subject: [PATCH 83/86] Add missing translation string to smarty (#130624) --- homeassistant/components/smarty/strings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json index 188459b4f16..341a300a26e 100644 --- a/homeassistant/components/smarty/strings.json +++ b/homeassistant/components/smarty/strings.json @@ -28,6 +28,10 @@ "deprecated_yaml_import_issue_auth_error": { "title": "YAML import failed due to an authentication error", "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "YAML import failed due to a connection error", + "description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." } }, "entity": { From c7ee7dc880a0952dcc8b447f70747980bbb56f88 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:26:05 +0100 Subject: [PATCH 84/86] Refactor translation checks (#130585) * Refactor translation checks * Adjust * Improve * Restore await * Delay pytest.fail until the end of the test --- tests/components/conftest.py | 155 ++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 5535ec3b976..363d39a2e63 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -26,7 +26,12 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType +from homeassistant.data_entry_flow import ( + FlowContext, + FlowHandler, + FlowManager, + FlowResultType, +) from homeassistant.helpers.translation import async_get_translations if TYPE_CHECKING: @@ -557,12 +562,12 @@ def _validate_translation_placeholders( description_placeholders is None or placeholder not in description_placeholders ): - pytest.fail( + ignore_translations[full_key] = ( f"Description not found for placeholder `{placeholder}` in {full_key}" ) -async def _ensure_translation_exists( +async def _validate_translation( hass: HomeAssistant, ignore_translations: dict[str, StoreInfo], category: str, @@ -588,7 +593,7 @@ async def _ensure_translation_exists( ignore_translations[full_key] = "used" return - pytest.fail( + ignore_translations[full_key] = ( f"Translation not found for {component}: `{category}.{key}`. " f"Please add to homeassistant/components/{component}/strings.json" ) @@ -604,84 +609,106 @@ def ignore_translations() -> str | list[str]: return [] +async def _check_config_flow_result_translations( + manager: FlowManager, + flow: FlowHandler, + result: FlowResult[FlowContext, str], + ignore_translations: dict[str, str], +) -> None: + if isinstance(manager, ConfigEntriesFlowManager): + category = "config" + integration = flow.handler + elif isinstance(manager, OptionsFlowManager): + category = "options" + integration = flow.hass.config_entries.async_get_entry(flow.handler).domain + else: + return + + # Check if this flow has been seen before + # Gets set to False on first run, and to True on subsequent runs + setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before")) + + if result["type"] is FlowResultType.FORM: + if step_id := result.get("step_id"): + # neither title nor description are required + # - title defaults to integration name + # - description is optional + for header in ("title", "description"): + await _validate_translation( + flow.hass, + ignore_translations, + category, + integration, + f"step.{step_id}.{header}", + result["description_placeholders"], + translation_required=False, + ) + if errors := result.get("errors"): + for error in errors.values(): + await _validate_translation( + flow.hass, + ignore_translations, + category, + integration, + f"error.{error}", + result["description_placeholders"], + ) + return + + if result["type"] is FlowResultType.ABORT: + # We don't need translations for a discovery flow which immediately + # aborts, since such flows won't be seen by users + if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES: + return + await _validate_translation( + flow.hass, + ignore_translations, + category, + integration, + f"abort.{result["reason"]}", + result["description_placeholders"], + ) + + @pytest.fixture(autouse=True) -def check_config_translations(ignore_translations: str | list[str]) -> Generator[None]: - """Ensure config_flow translations are available.""" +def check_translations(ignore_translations: str | list[str]) -> Generator[None]: + """Check that translation requirements are met. + + Current checks: + - data entry flow results (ConfigFlow/OptionsFlow) + """ if not isinstance(ignore_translations, list): ignore_translations = [ignore_translations] _ignore_translations = {k: "unused" for k in ignore_translations} - _original = FlowManager._async_handle_step - async def _async_handle_step( + # Keep reference to original functions + _original_flow_manager_async_handle_step = FlowManager._async_handle_step + + # Prepare override functions + async def _flow_manager_async_handle_step( self: FlowManager, flow: FlowHandler, *args ) -> FlowResult: - result = await _original(self, flow, *args) - if isinstance(self, ConfigEntriesFlowManager): - category = "config" - component = flow.handler - elif isinstance(self, OptionsFlowManager): - category = "options" - component = flow.hass.config_entries.async_get_entry(flow.handler).domain - else: - return result - - # Check if this flow has been seen before - # Gets set to False on first run, and to True on subsequent runs - setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before")) - - if result["type"] is FlowResultType.FORM: - if step_id := result.get("step_id"): - # neither title nor description are required - # - title defaults to integration name - # - description is optional - for header in ("title", "description"): - await _ensure_translation_exists( - flow.hass, - _ignore_translations, - category, - component, - f"step.{step_id}.{header}", - result["description_placeholders"], - translation_required=False, - ) - if errors := result.get("errors"): - for error in errors.values(): - await _ensure_translation_exists( - flow.hass, - _ignore_translations, - category, - component, - f"error.{error}", - result["description_placeholders"], - ) - return result - - if result["type"] is FlowResultType.ABORT: - # We don't need translations for a discovery flow which immediately - # aborts, since such flows won't be seen by users - if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES: - return result - await _ensure_translation_exists( - flow.hass, - _ignore_translations, - category, - component, - f"abort.{result["reason"]}", - result["description_placeholders"], - ) - + result = await _original_flow_manager_async_handle_step(self, flow, *args) + await _check_config_flow_result_translations( + self, flow, result, _ignore_translations + ) return result + # Use override functions with patch( "homeassistant.data_entry_flow.FlowManager._async_handle_step", - _async_handle_step, + _flow_manager_async_handle_step, ): yield + # Run final checks unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"] if unused_ignore: pytest.fail( f"Unused ignore translations: {', '.join(unused_ignore)}. " "Please remove them from the ignore_translations fixture." ) + for description in _ignore_translations.values(): + if description not in {"used", "unused"}: + pytest.fail(description) From cd1272008507c7cb82155a8d7509c95067290774 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 16:31:33 +0100 Subject: [PATCH 85/86] Add Python version to issue ID (#130611) --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index dcfb6685627..1034223051c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -515,7 +515,7 @@ async def async_from_config_dict( issue_registry.async_create_issue( hass, core.DOMAIN, - "python_version", + f"python_version_{required_python_version}", is_fixable=False, severity=issue_registry.IssueSeverity.WARNING, breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE, From 1ce8bfdaa438949da707d94ff7b12ff7b20ce0cc Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:34:17 +0100 Subject: [PATCH 86/86] Use test helpers for acaia buttons (#130626) --- .../acaia/snapshots/test_button.ambr | 60 +++++++++---------- tests/components/acaia/test_button.py | 33 ++++++---- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr index 7e2624923af..cd91ca1a17a 100644 --- a/tests/components/acaia/snapshots/test_button.ambr +++ b/tests/components/acaia/snapshots/test_button.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_buttons[entry_button_reset_timer] +# name: test_buttons[button.lunar_ddeeff_reset_timer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -32,7 +32,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[entry_button_start_stop_timer] +# name: test_buttons[button.lunar_ddeeff_reset_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Reset timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_reset_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.lunar_ddeeff_start_stop_timer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -65,7 +78,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[entry_button_tare] +# name: test_buttons[button.lunar_ddeeff_start_stop_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_start_stop_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.lunar_ddeeff_tare-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,33 +124,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[state_button_reset_timer] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Reset timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_reset_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[state_button_start_stop_timer] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_start_stop_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[state_button_tare] +# name: test_buttons[button.lunar_ddeeff_tare-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'LUNAR-DDEEFF Tare', diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py index 62eb8b61b8a..f68f85e253d 100644 --- a/tests/components/acaia/test_button.py +++ b/tests/components/acaia/test_button.py @@ -1,21 +1,24 @@ """Tests for the acaia buttons.""" from datetime import timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import async_fire_time_changed - -pytestmark = pytest.mark.usefixtures("init_integration") +from . import setup_integration +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform BUTTONS = ( "tare", @@ -28,24 +31,25 @@ async def test_buttons( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the acaia buttons.""" - for button in BUTTONS: - state = hass.states.get(f"button.lunar_ddeeff_{button}") - assert state - assert state == snapshot(name=f"state_button_{button}") - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot(name=f"entry_button_{button}") + with patch("homeassistant.components.acaia.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_button_presses( hass: HomeAssistant, mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the acaia button presses.""" + await setup_integration(hass, mock_config_entry) + for button in BUTTONS: await hass.services.async_call( BUTTON_DOMAIN, @@ -63,10 +67,13 @@ async def test_button_presses( async def test_buttons_unavailable_on_disconnected_scale( hass: HomeAssistant, mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: """Test the acaia buttons are unavailable when the scale is disconnected.""" + await setup_integration(hass, mock_config_entry) + for button in BUTTONS: state = hass.states.get(f"button.lunar_ddeeff_{button}") assert state