diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 581c84fe77a894..a1aa96cff71a8d 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.5.3"] + "requirements": ["airos==0.5.4"] } diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 9c1a2af7a9f7ca..ee94052c28663f 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -114,6 +114,8 @@ ), } +PARALLEL_UPDATES = 0 + @callback def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: diff --git a/homeassistant/components/asuswrt/helpers.py b/homeassistant/components/asuswrt/helpers.py index 0fb467e6046bd0..65ebedfab4d5bf 100644 --- a/homeassistant/components/asuswrt/helpers.py +++ b/homeassistant/components/asuswrt/helpers.py @@ -2,9 +2,7 @@ from __future__ import annotations -from typing import Any, TypeVar - -T = TypeVar("T", dict[str, Any], list[Any], None) +from typing import Any TRANSLATION_MAP = { "wan_rx": "sensor_rx_bytes", @@ -36,7 +34,7 @@ def clean_dict(raw: dict[str, Any]) -> dict[str, Any]: return {k: v for k, v in raw.items() if v is not None or k.endswith("state")} -def translate_to_legacy(raw: T) -> T: +def translate_to_legacy[T: (dict[str, Any], list[Any], None)](raw: T) -> T: """Translate raw data to legacy format for dicts and lists.""" if raw is None: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b54cca05c22101..fe7510c3bf578e 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -51,12 +51,6 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.deprecation import ( - DeprecatedConstantEnum, - all_with_deprecated_constants, - check_if_deprecated_constant, - dir_with_deprecated_constants, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval @@ -118,12 +112,6 @@ ATTR_MEDIA_PLAYER: Final = "media_player" ATTR_FORMAT: Final = "format" -# These constants are deprecated as of Home Assistant 2024.10 -# Please use the StreamType enum instead. -_DEPRECATED_STATE_RECORDING = DeprecatedConstantEnum(CameraState.RECORDING, "2025.10") -_DEPRECATED_STATE_STREAMING = DeprecatedConstantEnum(CameraState.STREAMING, "2025.10") -_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(CameraState.IDLE, "2025.10") - class CameraEntityFeature(IntFlag): """Supported features of the camera entity.""" @@ -1117,11 +1105,3 @@ async def async_handle_record_service( duration=service_call.data[CONF_DURATION], lookback=service_call.data[CONF_LOOKBACK], ) - - -# 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()) diff --git a/homeassistant/components/growatt_server/coordinator.py b/homeassistant/components/growatt_server/coordinator.py index a1a2fb938f0f40..931ae7e8bd54fa 100644 --- a/homeassistant/components/growatt_server/coordinator.py +++ b/homeassistant/components/growatt_server/coordinator.py @@ -1,5 +1,7 @@ """Coordinator module for managing Growatt data fetching.""" +from __future__ import annotations + import datetime import json import logging @@ -145,7 +147,7 @@ def get_currency(self): return self.data.get("currency") def get_data( - self, entity_description: "GrowattSensorEntityDescription" + self, entity_description: GrowattSensorEntityDescription ) -> str | int | float | None: """Get the data.""" variable = entity_description.api_key diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index cf78eaea05d18a..c6a419bba835ea 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["aiohasupervisor==0.3.3b0"], + "requirements": ["aiohasupervisor==0.3.3"], "single_config_entry": true } diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 8daa94f2f6d838..3df99e55aec6ab 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -11,7 +11,7 @@ from .const import CONF_DEVICE_DATA, CONF_DEVICE_TYPE from .coordinator import INKBIRDActiveBluetoothProcessorCoordinator -INKBIRDConfigEntry = ConfigEntry[INKBIRDActiveBluetoothProcessorCoordinator] +type INKBIRDConfigEntry = ConfigEntry[INKBIRDActiveBluetoothProcessorCoordinator] PLATFORMS: list[Platform] = [Platform.SENSOR] diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 05aed8a827f9e1..dcb2ed794e7e54 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -13,28 +13,16 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( # noqa: F401 - _DEPRECATED_STATE_JAMMED, - _DEPRECATED_STATE_LOCKED, - _DEPRECATED_STATE_LOCKING, - _DEPRECATED_STATE_UNLOCKED, - _DEPRECATED_STATE_UNLOCKING, +from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, - STATE_OPEN, - STATE_OPENING, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.deprecation import ( - all_with_deprecated_constants, - check_if_deprecated_constant, - dir_with_deprecated_constants, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType @@ -317,11 +305,3 @@ def _async_read_entity_options(self) -> None: return self._lock_option_default_code = "" - - -# These can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial( - dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] -) -__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/mcp/coordinator.py b/homeassistant/components/mcp/coordinator.py index f560875292f8aa..df4ec2c0f2f400 100644 --- a/homeassistant/components/mcp/coordinator.py +++ b/homeassistant/components/mcp/coordinator.py @@ -27,7 +27,7 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=30) TIMEOUT = 10 -TokenManager = Callable[[], Awaitable[str]] +type TokenManager = Callable[[], Awaitable[str]] @asynccontextmanager diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 01ff31e277c5ee..da773a7eb29f37 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -55,12 +55,6 @@ from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.deprecation import ( - DeprecatedConstantEnum, - all_with_deprecated_constants, - check_if_deprecated_constant, - dir_with_deprecated_constants, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url @@ -75,26 +69,6 @@ async_process_play_media_url, ) from .const import ( # noqa: F401 - _DEPRECATED_MEDIA_CLASS_DIRECTORY, - _DEPRECATED_SUPPORT_BROWSE_MEDIA, - _DEPRECATED_SUPPORT_CLEAR_PLAYLIST, - _DEPRECATED_SUPPORT_GROUPING, - _DEPRECATED_SUPPORT_NEXT_TRACK, - _DEPRECATED_SUPPORT_PAUSE, - _DEPRECATED_SUPPORT_PLAY, - _DEPRECATED_SUPPORT_PLAY_MEDIA, - _DEPRECATED_SUPPORT_PREVIOUS_TRACK, - _DEPRECATED_SUPPORT_REPEAT_SET, - _DEPRECATED_SUPPORT_SEEK, - _DEPRECATED_SUPPORT_SELECT_SOUND_MODE, - _DEPRECATED_SUPPORT_SELECT_SOURCE, - _DEPRECATED_SUPPORT_SHUFFLE_SET, - _DEPRECATED_SUPPORT_STOP, - _DEPRECATED_SUPPORT_TURN_OFF, - _DEPRECATED_SUPPORT_TURN_ON, - _DEPRECATED_SUPPORT_VOLUME_MUTE, - _DEPRECATED_SUPPORT_VOLUME_SET, - _DEPRECATED_SUPPORT_VOLUME_STEP, ATTR_APP_ID, ATTR_APP_NAME, ATTR_ENTITY_PICTURE_LOCAL, @@ -188,17 +162,6 @@ class MediaPlayerDeviceClass(StrEnum): DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass)) -# DEVICE_CLASS* below are deprecated as of 2021.12 -# use the MediaPlayerDeviceClass enum instead. -_DEPRECATED_DEVICE_CLASS_TV = DeprecatedConstantEnum( - MediaPlayerDeviceClass.TV, "2025.10" -) -_DEPRECATED_DEVICE_CLASS_SPEAKER = DeprecatedConstantEnum( - MediaPlayerDeviceClass.SPEAKER, "2025.10" -) -_DEPRECATED_DEVICE_CLASS_RECEIVER = DeprecatedConstantEnum( - MediaPlayerDeviceClass.RECEIVER, "2025.10" -) DEVICE_CLASSES = [cls.value for cls in MediaPlayerDeviceClass] @@ -1196,6 +1159,7 @@ async def async_internal_search_media( media_content_id: str | None = None, media_filter_classes: list[MediaClass] | None = None, ) -> SearchMedia: + """Search for media.""" return await self.async_search_media( query=SearchMediaQuery( search_query=search_query, @@ -1510,13 +1474,3 @@ async def async_fetch_image( logger.warning("Error retrieving proxied image from %s", url) return content, content_type - - -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# These can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial( - dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] -) -__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index f842ccccb65222..990acb4c497bf7 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,15 +1,8 @@ """Provides the constants needed for component.""" from enum import IntFlag, StrEnum -from functools import partial -from homeassistant.helpers.deprecation import ( - DeprecatedConstantEnum, - EnumWithDeprecatedMembers, - all_with_deprecated_constants, - check_if_deprecated_constant, - dir_with_deprecated_constants, -) +from homeassistant.helpers.deprecation import EnumWithDeprecatedMembers # How long our auth signature on the content should be valid for CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 @@ -94,38 +87,6 @@ class MediaClass(StrEnum): VIDEO = "video" -# These MEDIA_CLASS_* constants are deprecated as of Home Assistant 2022.10. -# Please use the MediaClass enum instead. -_DEPRECATED_MEDIA_CLASS_ALBUM = DeprecatedConstantEnum(MediaClass.ALBUM, "2025.10") -_DEPRECATED_MEDIA_CLASS_APP = DeprecatedConstantEnum(MediaClass.APP, "2025.10") -_DEPRECATED_MEDIA_CLASS_ARTIST = DeprecatedConstantEnum(MediaClass.ARTIST, "2025.10") -_DEPRECATED_MEDIA_CLASS_CHANNEL = DeprecatedConstantEnum(MediaClass.CHANNEL, "2025.10") -_DEPRECATED_MEDIA_CLASS_COMPOSER = DeprecatedConstantEnum( - MediaClass.COMPOSER, "2025.10" -) -_DEPRECATED_MEDIA_CLASS_CONTRIBUTING_ARTIST = DeprecatedConstantEnum( - MediaClass.CONTRIBUTING_ARTIST, "2025.10" -) -_DEPRECATED_MEDIA_CLASS_DIRECTORY = DeprecatedConstantEnum( - MediaClass.DIRECTORY, "2025.10" -) -_DEPRECATED_MEDIA_CLASS_EPISODE = DeprecatedConstantEnum(MediaClass.EPISODE, "2025.10") -_DEPRECATED_MEDIA_CLASS_GAME = DeprecatedConstantEnum(MediaClass.GAME, "2025.10") -_DEPRECATED_MEDIA_CLASS_GENRE = DeprecatedConstantEnum(MediaClass.GENRE, "2025.10") -_DEPRECATED_MEDIA_CLASS_IMAGE = DeprecatedConstantEnum(MediaClass.IMAGE, "2025.10") -_DEPRECATED_MEDIA_CLASS_MOVIE = DeprecatedConstantEnum(MediaClass.MOVIE, "2025.10") -_DEPRECATED_MEDIA_CLASS_MUSIC = DeprecatedConstantEnum(MediaClass.MUSIC, "2025.10") -_DEPRECATED_MEDIA_CLASS_PLAYLIST = DeprecatedConstantEnum( - MediaClass.PLAYLIST, "2025.10" -) -_DEPRECATED_MEDIA_CLASS_PODCAST = DeprecatedConstantEnum(MediaClass.PODCAST, "2025.10") -_DEPRECATED_MEDIA_CLASS_SEASON = DeprecatedConstantEnum(MediaClass.SEASON, "2025.10") -_DEPRECATED_MEDIA_CLASS_TRACK = DeprecatedConstantEnum(MediaClass.TRACK, "2025.10") -_DEPRECATED_MEDIA_CLASS_TV_SHOW = DeprecatedConstantEnum(MediaClass.TV_SHOW, "2025.10") -_DEPRECATED_MEDIA_CLASS_URL = DeprecatedConstantEnum(MediaClass.URL, "2025.10") -_DEPRECATED_MEDIA_CLASS_VIDEO = DeprecatedConstantEnum(MediaClass.VIDEO, "2025.10") - - class MediaType(StrEnum): """Media type for media player entities.""" @@ -152,33 +113,6 @@ class MediaType(StrEnum): VIDEO = "video" -# These MEDIA_TYPE_* constants are deprecated as of Home Assistant 2022.10. -# Please use the MediaType enum instead. -_DEPRECATED_MEDIA_TYPE_ALBUM = DeprecatedConstantEnum(MediaType.ALBUM, "2025.10") -_DEPRECATED_MEDIA_TYPE_APP = DeprecatedConstantEnum(MediaType.APP, "2025.10") -_DEPRECATED_MEDIA_TYPE_APPS = DeprecatedConstantEnum(MediaType.APPS, "2025.10") -_DEPRECATED_MEDIA_TYPE_ARTIST = DeprecatedConstantEnum(MediaType.ARTIST, "2025.10") -_DEPRECATED_MEDIA_TYPE_CHANNEL = DeprecatedConstantEnum(MediaType.CHANNEL, "2025.10") -_DEPRECATED_MEDIA_TYPE_CHANNELS = DeprecatedConstantEnum(MediaType.CHANNELS, "2025.10") -_DEPRECATED_MEDIA_TYPE_COMPOSER = DeprecatedConstantEnum(MediaType.COMPOSER, "2025.10") -_DEPRECATED_MEDIA_TYPE_CONTRIBUTING_ARTIST = DeprecatedConstantEnum( - MediaType.CONTRIBUTING_ARTIST, "2025.10" -) -_DEPRECATED_MEDIA_TYPE_EPISODE = DeprecatedConstantEnum(MediaType.EPISODE, "2025.10") -_DEPRECATED_MEDIA_TYPE_GAME = DeprecatedConstantEnum(MediaType.GAME, "2025.10") -_DEPRECATED_MEDIA_TYPE_GENRE = DeprecatedConstantEnum(MediaType.GENRE, "2025.10") -_DEPRECATED_MEDIA_TYPE_IMAGE = DeprecatedConstantEnum(MediaType.IMAGE, "2025.10") -_DEPRECATED_MEDIA_TYPE_MOVIE = DeprecatedConstantEnum(MediaType.MOVIE, "2025.10") -_DEPRECATED_MEDIA_TYPE_MUSIC = DeprecatedConstantEnum(MediaType.MUSIC, "2025.10") -_DEPRECATED_MEDIA_TYPE_PLAYLIST = DeprecatedConstantEnum(MediaType.PLAYLIST, "2025.10") -_DEPRECATED_MEDIA_TYPE_PODCAST = DeprecatedConstantEnum(MediaType.PODCAST, "2025.10") -_DEPRECATED_MEDIA_TYPE_SEASON = DeprecatedConstantEnum(MediaType.SEASON, "2025.10") -_DEPRECATED_MEDIA_TYPE_TRACK = DeprecatedConstantEnum(MediaType.TRACK, "2025.10") -_DEPRECATED_MEDIA_TYPE_TVSHOW = DeprecatedConstantEnum(MediaType.TVSHOW, "2025.10") -_DEPRECATED_MEDIA_TYPE_URL = DeprecatedConstantEnum(MediaType.URL, "2025.10") -_DEPRECATED_MEDIA_TYPE_VIDEO = DeprecatedConstantEnum(MediaType.VIDEO, "2025.10") - - SERVICE_CLEAR_PLAYLIST = "clear_playlist" SERVICE_JOIN = "join" SERVICE_PLAY_MEDIA = "play_media" @@ -197,11 +131,6 @@ class RepeatMode(StrEnum): ONE = "one" -# These REPEAT_MODE_* constants are deprecated as of Home Assistant 2022.10. -# Please use the RepeatMode enum instead. -_DEPRECATED_REPEAT_MODE_ALL = DeprecatedConstantEnum(RepeatMode.ALL, "2025.10") -_DEPRECATED_REPEAT_MODE_OFF = DeprecatedConstantEnum(RepeatMode.OFF, "2025.10") -_DEPRECATED_REPEAT_MODE_ONE = DeprecatedConstantEnum(RepeatMode.ONE, "2025.10") REPEAT_MODES = [cls.value for cls in RepeatMode] @@ -231,71 +160,3 @@ class MediaPlayerEntityFeature(IntFlag): MEDIA_ANNOUNCE = 1048576 MEDIA_ENQUEUE = 2097152 SEARCH_MEDIA = 4194304 - - -# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. -# Please use the MediaPlayerEntityFeature enum instead. -_DEPRECATED_SUPPORT_PAUSE = DeprecatedConstantEnum( - MediaPlayerEntityFeature.PAUSE, "2025.10" -) -_DEPRECATED_SUPPORT_SEEK = DeprecatedConstantEnum( - MediaPlayerEntityFeature.SEEK, "2025.10" -) -_DEPRECATED_SUPPORT_VOLUME_SET = DeprecatedConstantEnum( - MediaPlayerEntityFeature.VOLUME_SET, "2025.10" -) -_DEPRECATED_SUPPORT_VOLUME_MUTE = DeprecatedConstantEnum( - MediaPlayerEntityFeature.VOLUME_MUTE, "2025.10" -) -_DEPRECATED_SUPPORT_PREVIOUS_TRACK = DeprecatedConstantEnum( - MediaPlayerEntityFeature.PREVIOUS_TRACK, "2025.10" -) -_DEPRECATED_SUPPORT_NEXT_TRACK = DeprecatedConstantEnum( - MediaPlayerEntityFeature.NEXT_TRACK, "2025.10" -) -_DEPRECATED_SUPPORT_TURN_ON = DeprecatedConstantEnum( - MediaPlayerEntityFeature.TURN_ON, "2025.10" -) -_DEPRECATED_SUPPORT_TURN_OFF = DeprecatedConstantEnum( - MediaPlayerEntityFeature.TURN_OFF, "2025.10" -) -_DEPRECATED_SUPPORT_PLAY_MEDIA = DeprecatedConstantEnum( - MediaPlayerEntityFeature.PLAY_MEDIA, "2025.10" -) -_DEPRECATED_SUPPORT_VOLUME_STEP = DeprecatedConstantEnum( - MediaPlayerEntityFeature.VOLUME_STEP, "2025.10" -) -_DEPRECATED_SUPPORT_SELECT_SOURCE = DeprecatedConstantEnum( - MediaPlayerEntityFeature.SELECT_SOURCE, "2025.10" -) -_DEPRECATED_SUPPORT_STOP = DeprecatedConstantEnum( - MediaPlayerEntityFeature.STOP, "2025.10" -) -_DEPRECATED_SUPPORT_CLEAR_PLAYLIST = DeprecatedConstantEnum( - MediaPlayerEntityFeature.CLEAR_PLAYLIST, "2025.10" -) -_DEPRECATED_SUPPORT_PLAY = DeprecatedConstantEnum( - MediaPlayerEntityFeature.PLAY, "2025.10" -) -_DEPRECATED_SUPPORT_SHUFFLE_SET = DeprecatedConstantEnum( - MediaPlayerEntityFeature.SHUFFLE_SET, "2025.10" -) -_DEPRECATED_SUPPORT_SELECT_SOUND_MODE = DeprecatedConstantEnum( - MediaPlayerEntityFeature.SELECT_SOUND_MODE, "2025.10" -) -_DEPRECATED_SUPPORT_BROWSE_MEDIA = DeprecatedConstantEnum( - MediaPlayerEntityFeature.BROWSE_MEDIA, "2025.10" -) -_DEPRECATED_SUPPORT_REPEAT_SET = DeprecatedConstantEnum( - MediaPlayerEntityFeature.REPEAT_SET, "2025.10" -) -_DEPRECATED_SUPPORT_GROUPING = DeprecatedConstantEnum( - MediaPlayerEntityFeature.GROUPING, "2025.10" -) - -# 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()) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 5a25870512e184..4208c098902a3a 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -208,7 +208,7 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: def __process_raw_value(self, entry: float | str | bytes) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" - if self._nan_value and entry in (self._nan_value, -self._nan_value): + if self._nan_value is not None and entry in (self._nan_value, -self._nan_value): return None if isinstance(entry, bytes): return entry.decode() diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 77564245522978..92ee6d782ea6f7 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -115,9 +115,7 @@ def __init__(self, name: str) -> None: async def _start_pianobar(self) -> bool: pianobar = pexpect.spawn("pianobar", encoding="utf-8") pianobar.delaybeforesend = None - # mypy thinks delayafterread must be a float but that is not what pexpect says - # https://github.com/pexpect/pexpect/blob/4.9/pexpect/expect.py#L170 - pianobar.delayafterread = None # type: ignore[assignment] + pianobar.delayafterread = None pianobar.delayafterclose = 0 pianobar.delayafterterminate = 0 _LOGGER.debug("Started pianobar subprocess") diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 11fa530f47b126..00b399579842c9 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -215,6 +215,7 @@ def poll_api() -> dict[str, Any] | None: return DataUpdateCoordinator( hass, _LOGGER, + config_entry=None, name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 3f504b4c2fbcad..784af0594fbf15 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable import re -from typing import Generic, TypeVar, cast +from typing import cast from qbusmqttapi.discovery import QbusMqttDevice, QbusMqttOutput from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory @@ -20,8 +20,6 @@ _REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$") -StateT = TypeVar("StateT", bound=QbusMqttState) - def create_new_entities( coordinator: QbusControllerCoordinator, @@ -78,7 +76,7 @@ def create_unique_id(serial_number: str, suffix: str) -> str: return f"ctd_{serial_number}_{suffix}" -class QbusEntity(Entity, ABC, Generic[StateT]): +class QbusEntity[StateT: QbusMqttState](Entity, ABC): """Representation of a Qbus entity.""" _state_cls: type[StateT] = cast(type[StateT], QbusMqttState) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 34fa6a62d44b0e..c88a65b78c6678 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1127,9 +1127,6 @@ def _process_state_changed_event_into_session( else: states_manager.add_pending(entity_id, dbstate) - if states_meta_manager.active: - dbstate.entity_id = None - if entity_id is None or not ( shared_attrs_bytes := state_attributes_manager.serialize_from_event(event) ): @@ -1140,7 +1137,7 @@ def _process_state_changed_event_into_session( dbstate.states_meta_rel = pending_states_meta elif metadata_id := states_meta_manager.get(entity_id, session, True): dbstate.metadata_id = metadata_id - elif states_meta_manager.active and entity_removed: + elif entity_removed: # If the entity was removed, we don't need to add it to the # StatesMeta table or record it in the pending commit # if it does not have a metadata_id allocated to it as diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 6566cadf64c279..a0e82de9fe02fd 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta import logging import time -from typing import Any, Final, Protocol, Self, cast +from typing import Any, Final, Protocol, Self import ciso8601 from fnv_hash_fast import fnv1a_32 @@ -45,14 +45,9 @@ MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, ) -from homeassistant.core import Context, Event, EventOrigin, EventStateChangedData, State +from homeassistant.core import Event, EventStateChangedData from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null from homeassistant.util import dt as dt_util -from homeassistant.util.json import ( - JSON_DECODE_EXCEPTIONS, - json_loads, - json_loads_object, -) from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect from .models import ( @@ -60,8 +55,6 @@ StatisticDataTimestamp, StatisticMeanType, StatisticMetaData, - bytes_to_ulid_or_none, - bytes_to_uuid_hex_or_none, datetime_to_timestamp_or_none, process_timestamp, ulid_to_bytes_or_none, @@ -251,9 +244,6 @@ def process(value: Any) -> str: return process -EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] - - class Events(Base): """Event history data.""" @@ -333,28 +323,6 @@ def from_event(event: Event) -> Events: context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), ) - def to_native(self, validate_entity_id: bool = True) -> Event | None: - """Convert to a native HA Event.""" - context = Context( - id=bytes_to_ulid_or_none(self.context_id_bin), - user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), - parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), - ) - try: - return Event( - self.event_type or "", - json_loads_object(self.event_data) if self.event_data else {}, - EventOrigin(self.origin) - if self.origin - else EVENT_ORIGIN_ORDER[self.origin_idx or 0], - self.time_fired_ts or 0, - context=context, - ) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting to event: %s", self) - return None - class LegacyEvents(LegacyBase): """Event history data with event_id, used for schema migration.""" @@ -410,17 +378,6 @@ def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: """Return the hash of json encoded shared data.""" return fnv1a_32(shared_data_bytes) - def to_native(self) -> dict[str, Any]: - """Convert to an event data dictionary.""" - shared_data = self.shared_data - if shared_data is None: - return {} - try: - return cast(dict[str, Any], json_loads(shared_data)) - except JSON_DECODE_EXCEPTIONS: - _LOGGER.exception("Error converting row to event data: %s", self) - return {} - class EventTypes(Base): """Event type history.""" @@ -537,7 +494,7 @@ def from_event(event: Event[EventStateChangedData]) -> States: context = event.context return States( state=state_value, - entity_id=event.data["entity_id"], + entity_id=None, attributes=None, context_id=None, context_id_bin=ulid_to_bytes_or_none(context.id), @@ -553,44 +510,6 @@ def from_event(event: Event[EventStateChangedData]) -> States: last_reported_ts=last_reported_ts, ) - def to_native(self, validate_entity_id: bool = True) -> State | None: - """Convert to an HA state object.""" - context = Context( - id=bytes_to_ulid_or_none(self.context_id_bin), - user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), - parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), - ) - try: - attrs = json_loads_object(self.attributes) if self.attributes else {} - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state: %s", self) - return None - last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) - if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: - last_changed = dt_util.utc_from_timestamp(self.last_updated_ts or 0) - else: - last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) - if ( - self.last_reported_ts is None - or self.last_reported_ts == self.last_updated_ts - ): - last_reported = dt_util.utc_from_timestamp(self.last_updated_ts or 0) - else: - last_reported = dt_util.utc_from_timestamp(self.last_reported_ts or 0) - return State( - self.entity_id or "", - self.state, # type: ignore[arg-type] - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - attrs, - last_changed=last_changed, - last_reported=last_reported, - last_updated=last_updated, - context=context, - validate_entity_id=validate_entity_id, - ) - class LegacyStates(LegacyBase): """State change history with entity_id, used for schema migration.""" @@ -675,18 +594,6 @@ def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: """Return the hash of json encoded shared attributes.""" return fnv1a_32(shared_attrs_bytes) - def to_native(self) -> dict[str, Any]: - """Convert to a state attributes dictionary.""" - shared_attrs = self.shared_attrs - if shared_attrs is None: - return {} - try: - return cast(dict[str, Any], json_loads(shared_attrs)) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state attributes: %s", self) - return {} - class StatesMeta(Base): """Metadata for states.""" @@ -903,10 +810,6 @@ def __repr__(self) -> str: f" created='{self.created.isoformat(sep=' ', timespec='seconds')}')>" ) - def to_native(self, validate_entity_id: bool = True) -> Self: - """Return self, native format is this model.""" - return self - class MigrationChanges(Base): """Representation of migration changes.""" diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index 30a3a1b8239034..904582b75f062b 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -61,15 +61,6 @@ def update_states_metadata( ) -> None: """Update the states metadata table when an entity is renamed.""" states_meta_manager = instance.states_meta_manager - if not states_meta_manager.active: - _LOGGER.warning( - "Cannot rename entity_id `%s` to `%s` " - "because the states meta manager is not yet active", - entity_id, - new_entity_id, - ) - return - with session_scope( session=instance.get_session(), exception_filter=filter_unique_constraint_integrity_error(instance, "state"), diff --git a/homeassistant/components/recorder/history/__init__.py b/homeassistant/components/recorder/history/__init__.py index 469d6694640740..20453a0b1c892f 100644 --- a/homeassistant/components/recorder/history/__init__.py +++ b/homeassistant/components/recorder/history/__init__.py @@ -8,7 +8,6 @@ from sqlalchemy.orm.session import Session from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.recorder import get_instance from ..filters import Filters from .const import NEED_ATTRIBUTE_DOMAINS, SIGNIFICANT_DOMAINS @@ -44,15 +43,7 @@ def get_full_significant_states_with_session( no_attributes: bool = False, ) -> dict[str, list[State]]: """Return a dict of significant states during a time period.""" - if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # noqa: PLC0415 - get_full_significant_states_with_session as _legacy_get_full_significant_states_with_session, - ) - - _target = _legacy_get_full_significant_states_with_session - else: - _target = _modern_get_full_significant_states_with_session - return _target( + return _modern_get_full_significant_states_with_session( hass, session, start_time, @@ -69,15 +60,7 @@ def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> dict[str, list[State]]: """Return the last number_of_states.""" - if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # noqa: PLC0415 - get_last_state_changes as _legacy_get_last_state_changes, - ) - - _target = _legacy_get_last_state_changes - else: - _target = _modern_get_last_state_changes - return _target(hass, number_of_states, entity_id) + return _modern_get_last_state_changes(hass, number_of_states, entity_id) def get_significant_states( @@ -93,15 +76,7 @@ def get_significant_states( compressed_state_format: bool = False, ) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" - if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # noqa: PLC0415 - get_significant_states as _legacy_get_significant_states, - ) - - _target = _legacy_get_significant_states - else: - _target = _modern_get_significant_states - return _target( + return _modern_get_significant_states( hass, start_time, end_time, @@ -129,15 +104,7 @@ def get_significant_states_with_session( compressed_state_format: bool = False, ) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" - if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # noqa: PLC0415 - get_significant_states_with_session as _legacy_get_significant_states_with_session, - ) - - _target = _legacy_get_significant_states_with_session - else: - _target = _modern_get_significant_states_with_session - return _target( + return _modern_get_significant_states_with_session( hass, session, start_time, @@ -163,15 +130,7 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> dict[str, list[State]]: """Return a list of states that changed during a time period.""" - if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # noqa: PLC0415 - state_changes_during_period as _legacy_state_changes_during_period, - ) - - _target = _legacy_state_changes_during_period - else: - _target = _modern_state_changes_during_period - return _target( + return _modern_state_changes_during_period( hass, start_time, end_time, diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py deleted file mode 100644 index 4323ad9466b385..00000000000000 --- a/homeassistant/components/recorder/history/legacy.py +++ /dev/null @@ -1,664 +0,0 @@ -"""Provide pre-made queries on top of the recorder component.""" - -from __future__ import annotations - -from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator -from datetime import datetime -from itertools import groupby -from operator import attrgetter -import time -from typing import Any, cast - -from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select -from sqlalchemy.engine.row import Row -from sqlalchemy.orm.properties import MappedColumn -from sqlalchemy.orm.session import Session -from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement - -from homeassistant.const import COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE -from homeassistant.core import HomeAssistant, State, split_entity_id -from homeassistant.helpers.recorder import get_instance -from homeassistant.util import dt as dt_util - -from ..db_schema import StateAttributes, States -from ..filters import Filters -from ..models import process_timestamp_to_utc_isoformat -from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state -from ..util import execute_stmt_lambda_element, session_scope -from .const import ( - LAST_CHANGED_KEY, - NEED_ATTRIBUTE_DOMAINS, - SIGNIFICANT_DOMAINS, - SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE, - STATE_KEY, -) - -_BASE_STATES = ( - States.entity_id, - States.state, - States.last_changed_ts, - States.last_updated_ts, -) -_BASE_STATES_NO_LAST_CHANGED = ( - States.entity_id, - States.state, - literal(value=None).label("last_changed_ts"), - States.last_updated_ts, -) -_QUERY_STATE_NO_ATTR = ( - *_BASE_STATES, - literal(value=None, type_=Text).label("attributes"), - literal(value=None, type_=Text).label("shared_attrs"), -) -_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED = ( - *_BASE_STATES_NO_LAST_CHANGED, - literal(value=None, type_=Text).label("attributes"), - literal(value=None, type_=Text).label("shared_attrs"), -) -_BASE_STATES_PRE_SCHEMA_31 = ( - States.entity_id, - States.state, - States.last_changed, - States.last_updated, -) -_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = ( - States.entity_id, - States.state, - literal(value=None, type_=Text).label("last_changed"), - States.last_updated, -) -_QUERY_STATE_NO_ATTR_PRE_SCHEMA_31 = ( - *_BASE_STATES_PRE_SCHEMA_31, - literal(value=None, type_=Text).label("attributes"), - literal(value=None, type_=Text).label("shared_attrs"), -) -_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED_PRE_SCHEMA_31 = ( - *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, - literal(value=None, type_=Text).label("attributes"), - literal(value=None, type_=Text).label("shared_attrs"), -) -# Remove QUERY_STATES_PRE_SCHEMA_25 -# and the migration_in_progress check -# once schema 26 is created -_QUERY_STATES_PRE_SCHEMA_25 = ( - *_BASE_STATES_PRE_SCHEMA_31, - States.attributes, - literal(value=None, type_=Text).label("shared_attrs"), -) -_QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = ( - *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, - States.attributes, - literal(value=None, type_=Text).label("shared_attrs"), -) -_QUERY_STATES_PRE_SCHEMA_31 = ( - *_BASE_STATES_PRE_SCHEMA_31, - # Remove States.attributes once all attributes are in StateAttributes.shared_attrs - States.attributes, - StateAttributes.shared_attrs, -) -_QUERY_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = ( - *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, - # Remove States.attributes once all attributes are in StateAttributes.shared_attrs - States.attributes, - StateAttributes.shared_attrs, -) -_QUERY_STATES = ( - *_BASE_STATES, - # Remove States.attributes once all attributes are in StateAttributes.shared_attrs - States.attributes, - StateAttributes.shared_attrs, -) -_QUERY_STATES_NO_LAST_CHANGED = ( - *_BASE_STATES_NO_LAST_CHANGED, - # Remove States.attributes once all attributes are in StateAttributes.shared_attrs - States.attributes, - StateAttributes.shared_attrs, -) -_FIELD_MAP = { - cast(MappedColumn, field).name: idx - for idx, field in enumerate(_QUERY_STATE_NO_ATTR) -} -_FIELD_MAP_PRE_SCHEMA_31 = { - cast(MappedColumn, field).name: idx - for idx, field in enumerate(_QUERY_STATES_PRE_SCHEMA_31) -} - - -def _lambda_stmt_and_join_attributes( - no_attributes: bool, include_last_changed: bool = True -) -> tuple[StatementLambdaElement, bool]: - """Return the lambda_stmt and if StateAttributes should be joined. - - Because these are lambda_stmt the values inside the lambdas need - to be explicitly written out to avoid caching the wrong values. - """ - # If no_attributes was requested we do the query - # without the attributes fields and do not join the - # state_attributes table - if no_attributes: - if include_last_changed: - return ( - lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR)), - False, - ) - return ( - lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), - False, - ) - - if include_last_changed: - return lambda_stmt(lambda: select(*_QUERY_STATES)), True - return lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED)), True - - -def get_significant_states( - hass: HomeAssistant, - start_time: datetime, - end_time: datetime | None = None, - entity_ids: list[str] | None = None, - filters: Filters | None = None, - include_start_time_state: bool = True, - significant_changes_only: bool = True, - minimal_response: bool = False, - no_attributes: bool = False, - compressed_state_format: bool = False, -) -> dict[str, list[State | dict[str, Any]]]: - """Wrap get_significant_states_with_session with an sql session.""" - with session_scope(hass=hass, read_only=True) as session: - return get_significant_states_with_session( - hass, - session, - start_time, - end_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - compressed_state_format, - ) - - -def _significant_states_stmt( - start_time: datetime, - end_time: datetime | None, - entity_ids: list[str], - significant_changes_only: bool, - no_attributes: bool, -) -> StatementLambdaElement: - """Query the database for significant state changes.""" - stmt, join_attributes = _lambda_stmt_and_join_attributes( - no_attributes, include_last_changed=not significant_changes_only - ) - if ( - len(entity_ids) == 1 - and significant_changes_only - and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS - ): - stmt += lambda q: q.filter( - (States.last_changed_ts == States.last_updated_ts) - | States.last_changed_ts.is_(None) - ) - elif significant_changes_only: - stmt += lambda q: q.filter( - or_( - *[ - States.entity_id.like(entity_domain) - for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE - ], - ( - (States.last_changed_ts == States.last_updated_ts) - | States.last_changed_ts.is_(None) - ), - ) - ) - stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) - - start_time_ts = start_time.timestamp() - stmt += lambda q: q.filter(States.last_updated_ts > start_time_ts) - if end_time: - end_time_ts = end_time.timestamp() - stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) - - if join_attributes: - stmt += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) - return stmt - - -def get_significant_states_with_session( - hass: HomeAssistant, - session: Session, - start_time: datetime, - end_time: datetime | None = None, - entity_ids: list[str] | None = None, - filters: Filters | None = None, - include_start_time_state: bool = True, - significant_changes_only: bool = True, - minimal_response: bool = False, - no_attributes: bool = False, - compressed_state_format: bool = False, -) -> dict[str, list[State | dict[str, Any]]]: - """Return states changes during UTC period start_time - end_time. - - entity_ids is an optional iterable of entities to include in the results. - - filters is an optional SQLAlchemy filter which will be applied to the database - queries unless entity_ids is given, in which case its ignored. - - Significant states are all states where there is a state change, - as well as all states from certain domains (for instance - thermostat so that we get current temperature in our graphs). - """ - if filters is not None: - raise NotImplementedError("Filters are no longer supported") - if not entity_ids: - raise ValueError("entity_ids must be provided") - stmt = _significant_states_stmt( - start_time, - end_time, - entity_ids, - significant_changes_only, - no_attributes, - ) - states = execute_stmt_lambda_element(session, stmt, None, end_time) - return _sorted_states_to_dict( - hass, - session, - states, - start_time, - entity_ids, - include_start_time_state, - minimal_response, - no_attributes, - compressed_state_format, - ) - - -def get_full_significant_states_with_session( - hass: HomeAssistant, - session: Session, - start_time: datetime, - end_time: datetime | None = None, - entity_ids: list[str] | None = None, - filters: Filters | None = None, - include_start_time_state: bool = True, - significant_changes_only: bool = True, - no_attributes: bool = False, -) -> dict[str, list[State]]: - """Variant of get_significant_states_with_session. - - Difference with get_significant_states_with_session is that it does not - return minimal responses. - """ - return cast( - dict[str, list[State]], - get_significant_states_with_session( - hass=hass, - session=session, - start_time=start_time, - end_time=end_time, - entity_ids=entity_ids, - filters=filters, - include_start_time_state=include_start_time_state, - significant_changes_only=significant_changes_only, - minimal_response=False, - no_attributes=no_attributes, - ), - ) - - -def _state_changed_during_period_stmt( - start_time: datetime, - end_time: datetime | None, - entity_id: str, - no_attributes: bool, - descending: bool, - limit: int | None, -) -> StatementLambdaElement: - stmt, join_attributes = _lambda_stmt_and_join_attributes( - no_attributes, include_last_changed=False - ) - start_time_ts = start_time.timestamp() - stmt += lambda q: q.filter( - ( - (States.last_changed_ts == States.last_updated_ts) - | States.last_changed_ts.is_(None) - ) - & (States.last_updated_ts > start_time_ts) - ) - if end_time: - end_time_ts = end_time.timestamp() - stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) - stmt += lambda q: q.filter(States.entity_id == entity_id) - if join_attributes: - stmt += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts.desc()) - else: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) - - if limit: - stmt += lambda q: q.limit(limit) - return stmt - - -def state_changes_during_period( - hass: HomeAssistant, - start_time: datetime, - end_time: datetime | None = None, - entity_id: str | None = None, - no_attributes: bool = False, - descending: bool = False, - limit: int | None = None, - include_start_time_state: bool = True, -) -> dict[str, list[State]]: - """Return states changes during UTC period start_time - end_time.""" - if not entity_id: - raise ValueError("entity_id must be provided") - entity_ids = [entity_id.lower()] - with session_scope(hass=hass, read_only=True) as session: - stmt = _state_changed_during_period_stmt( - start_time, - end_time, - entity_id, - no_attributes, - descending, - limit, - ) - states = execute_stmt_lambda_element(session, stmt, None, end_time) - return cast( - dict[str, list[State]], - _sorted_states_to_dict( - hass, - session, - states, - start_time, - entity_ids, - include_start_time_state=include_start_time_state, - ), - ) - - -def _get_last_state_changes_stmt( - number_of_states: int, entity_id: str -) -> StatementLambdaElement: - stmt, join_attributes = _lambda_stmt_and_join_attributes( - False, include_last_changed=False - ) - stmt += lambda q: q.where( - States.state_id - == ( - select(States.state_id) - .filter(States.entity_id == entity_id) - .order_by(States.last_updated_ts.desc()) - .limit(number_of_states) - .subquery() - ).c.state_id - ) - if join_attributes: - stmt += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - - stmt += lambda q: q.order_by(States.state_id.desc()) - return stmt - - -def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str -) -> dict[str, list[State]]: - """Return the last number_of_states.""" - entity_id_lower = entity_id.lower() - entity_ids = [entity_id_lower] - - with session_scope(hass=hass, read_only=True) as session: - stmt = _get_last_state_changes_stmt(number_of_states, entity_id_lower) - states = list(execute_stmt_lambda_element(session, stmt)) - return cast( - dict[str, list[State]], - _sorted_states_to_dict( - hass, - session, - reversed(states), - dt_util.utcnow(), - entity_ids, - include_start_time_state=False, - ), - ) - - -def _get_states_for_entities_stmt( - run_start_ts: float, - utc_point_in_time: datetime, - entity_ids: list[str], - no_attributes: bool, -) -> StatementLambdaElement: - """Baked query to get states for specific entities.""" - stmt, join_attributes = _lambda_stmt_and_join_attributes( - no_attributes, include_last_changed=True - ) - # We got an include-list of entities, accelerate the query by filtering already - # in the inner query. - utc_point_in_time_ts = utc_point_in_time.timestamp() - stmt += lambda q: q.join( - ( - most_recent_states_for_entities_by_date := ( - select( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated_ts).label("max_last_updated"), - ) - .filter( - (States.last_updated_ts >= run_start_ts) - & (States.last_updated_ts < utc_point_in_time_ts) - ) - .filter(States.entity_id.in_(entity_ids)) - .group_by(States.entity_id) - .subquery() - ) - ), - and_( - States.entity_id == most_recent_states_for_entities_by_date.c.max_entity_id, - States.last_updated_ts - == most_recent_states_for_entities_by_date.c.max_last_updated, - ), - ) - if join_attributes: - stmt += lambda q: q.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) - return stmt - - -def _get_rows_with_session( - hass: HomeAssistant, - session: Session, - utc_point_in_time: datetime, - entity_ids: list[str], - *, - no_attributes: bool = False, -) -> Iterable[Row]: - """Return the states at a specific point in time.""" - if len(entity_ids) == 1: - return execute_stmt_lambda_element( - session, - _get_single_entity_states_stmt( - utc_point_in_time, entity_ids[0], no_attributes - ), - ) - - oldest_ts = get_instance(hass).states_manager.oldest_ts - - if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp(): - # We don't have any states for the requested time - return [] - - # We have more than one entity to look at so we need to do a query on states - # since the last recorder run started. - stmt = _get_states_for_entities_stmt( - oldest_ts, utc_point_in_time, entity_ids, no_attributes - ) - return execute_stmt_lambda_element(session, stmt) - - -def _get_single_entity_states_stmt( - utc_point_in_time: datetime, - entity_id: str, - no_attributes: bool = False, -) -> StatementLambdaElement: - # Use an entirely different (and extremely fast) query if we only - # have a single entity id - stmt, join_attributes = _lambda_stmt_and_join_attributes( - no_attributes, include_last_changed=True - ) - utc_point_in_time_ts = utc_point_in_time.timestamp() - stmt += ( - lambda q: q.filter( - States.last_updated_ts < utc_point_in_time_ts, - States.entity_id == entity_id, - ) - .order_by(States.last_updated_ts.desc()) - .limit(1) - ) - if join_attributes: - stmt += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - return stmt - - -def _sorted_states_to_dict( - hass: HomeAssistant, - session: Session, - states: Iterable[Row], - start_time: datetime, - entity_ids: list[str], - include_start_time_state: bool = True, - minimal_response: bool = False, - no_attributes: bool = False, - compressed_state_format: bool = False, -) -> dict[str, list[State | dict[str, Any]]]: - """Convert SQL results into JSON friendly data structure. - - This takes our state list and turns it into a JSON friendly data - structure {'entity_id': [list of states], 'entity_id2': [list of states]} - - States must be sorted by entity_id and last_updated - - We also need to go back and create a synthetic zero data point for - each list of states, otherwise our graphs won't start on the Y - axis correctly. - """ - state_class: Callable[ - [Row, dict[str, dict[str, Any]], datetime | None], State | dict[str, Any] - ] - if compressed_state_format: - state_class = legacy_row_to_compressed_state - attr_time = COMPRESSED_STATE_LAST_UPDATED - attr_state = COMPRESSED_STATE_STATE - else: - state_class = LegacyLazyState - attr_time = LAST_CHANGED_KEY - attr_state = STATE_KEY - - result: dict[str, list[State | dict[str, Any]]] = defaultdict(list) - # Set all entity IDs to empty lists in result set to maintain the order - for ent_id in entity_ids: - result[ent_id] = [] - - # Get the states at the start time - time.perf_counter() - initial_states: dict[str, Row] = {} - if include_start_time_state: - initial_states = { - row.entity_id: row - for row in _get_rows_with_session( - hass, - session, - start_time, - entity_ids, - no_attributes=no_attributes, - ) - } - - if len(entity_ids) == 1: - states_iter: Iterable[tuple[str, Iterator[Row]]] = ( - (entity_ids[0], iter(states)), - ) - else: - key_func = attrgetter("entity_id") - states_iter = groupby(states, key_func) - - # Append all changes to it - for ent_id, group in states_iter: - attr_cache: dict[str, dict[str, Any]] = {} - prev_state: Column | str - ent_results = result[ent_id] - if row := initial_states.pop(ent_id, None): - prev_state = row.state - ent_results.append(state_class(row, attr_cache, start_time)) - - if not minimal_response or split_entity_id(ent_id)[0] in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend( - state_class(db_state, attr_cache, None) for db_state in group - ) - continue - - # With minimal response we only provide a native - # State for the first and last response. All the states - # in-between only provide the "state" and the - # "last_changed". - if not ent_results: - if (first_state := next(group, None)) is None: - continue - prev_state = first_state.state - ent_results.append(state_class(first_state, attr_cache, None)) - - state_idx = _FIELD_MAP["state"] - - # - # minimal_response only makes sense with last_updated == last_updated - # - # We use last_updated for for last_changed since its the same - # - # With minimal response we do not care about attribute - # changes so we can filter out duplicate states - last_updated_ts_idx = _FIELD_MAP["last_updated_ts"] - if compressed_state_format: - for row in group: - if (state := row[state_idx]) != prev_state: - ent_results.append( - { - attr_state: state, - attr_time: row[last_updated_ts_idx], - } - ) - prev_state = state - continue - - for row in group: - if (state := row[state_idx]) != prev_state: - ent_results.append( - { - attr_state: state, - attr_time: process_timestamp_to_utc_isoformat( - dt_util.utc_from_timestamp(row[last_updated_ts_idx]) - ), - } - ) - prev_state = state - - # If there are no states beyond the initial state, - # the state a was never popped from initial_states - for ent_id, row in initial_states.items(): - result[ent_id].append(state_class(row, {}, start_time)) - - # Filter out the empty lists if some states had 0 results. - return {key: val for key, val in result.items() if val} diff --git a/homeassistant/components/recorder/models/legacy.py b/homeassistant/components/recorder/models/legacy.py deleted file mode 100644 index 11ea9141fc082d..00000000000000 --- a/homeassistant/components/recorder/models/legacy.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Models for Recorder.""" - -from __future__ import annotations - -from datetime import datetime -from typing import Any - -from sqlalchemy.engine.row import Row - -from homeassistant.const import ( - COMPRESSED_STATE_ATTRIBUTES, - COMPRESSED_STATE_LAST_CHANGED, - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, -) -from homeassistant.core import Context, State -from homeassistant.util import dt as dt_util - -from .state_attributes import decode_attributes_from_source -from .time import process_timestamp - - -class LegacyLazyState(State): - """A lazy version of core State after schema 31.""" - - __slots__ = [ - "_attributes", - "_context", - "_last_changed_ts", - "_last_reported_ts", - "_last_updated_ts", - "_row", - "attr_cache", - ] - - def __init__( # pylint: disable=super-init-not-called - self, - row: Row, - attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None, - entity_id: str | None = None, - ) -> None: - """Init the lazy state.""" - self._row = row - self.entity_id = entity_id or self._row.entity_id - self.state = self._row.state or "" - self._attributes: dict[str, Any] | None = None - self._last_updated_ts: float | None = self._row.last_updated_ts or ( - start_time.timestamp() if start_time else None - ) - self._last_changed_ts: float | None = ( - self._row.last_changed_ts or self._last_updated_ts - ) - self._last_reported_ts: float | None = self._last_updated_ts - self._context: Context | None = None - self.attr_cache = attr_cache - - @property # type: ignore[override] - def attributes(self) -> dict[str, Any]: - """State attributes.""" - if self._attributes is None: - self._attributes = decode_attributes_from_row_legacy( - self._row, self.attr_cache - ) - return self._attributes - - @attributes.setter - def attributes(self, value: dict[str, Any]) -> None: - """Set attributes.""" - self._attributes = value - - @property - def context(self) -> Context: - """State context.""" - if self._context is None: - self._context = Context(id=None) - return self._context - - @context.setter - def context(self, value: Context) -> None: - """Set context.""" - self._context = value - - @property - def last_changed(self) -> datetime: - """Last changed datetime.""" - assert self._last_changed_ts is not None - return dt_util.utc_from_timestamp(self._last_changed_ts) - - @last_changed.setter - def last_changed(self, value: datetime) -> None: - """Set last changed datetime.""" - self._last_changed_ts = process_timestamp(value).timestamp() - - @property - def last_reported(self) -> datetime: - """Last reported datetime.""" - assert self._last_reported_ts is not None - return dt_util.utc_from_timestamp(self._last_reported_ts) - - @last_reported.setter - def last_reported(self, value: datetime) -> None: - """Set last reported datetime.""" - self._last_reported_ts = process_timestamp(value).timestamp() - - @property - def last_updated(self) -> datetime: - """Last updated datetime.""" - assert self._last_updated_ts is not None - return dt_util.utc_from_timestamp(self._last_updated_ts) - - @last_updated.setter - def last_updated(self, value: datetime) -> None: - """Set last updated datetime.""" - self._last_updated_ts = process_timestamp(value).timestamp() - - def as_dict(self) -> dict[str, Any]: # type: ignore[override] - """Return a dict representation of the LazyState. - - Async friendly. - To be used for JSON serialization. - """ - last_updated_isoformat = self.last_updated.isoformat() - if self._last_changed_ts == self._last_updated_ts: - last_changed_isoformat = last_updated_isoformat - else: - last_changed_isoformat = self.last_changed.isoformat() - return { - "entity_id": self.entity_id, - "state": self.state, - "attributes": self._attributes or self.attributes, - "last_changed": last_changed_isoformat, - "last_updated": last_updated_isoformat, - } - - -def legacy_row_to_compressed_state( - row: Row, - attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None, - entity_id: str | None = None, -) -> dict[str, Any]: - """Convert a database row to a compressed state schema 31 and later.""" - comp_state = { - COMPRESSED_STATE_STATE: row.state, - COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row_legacy(row, attr_cache), - } - if start_time: - comp_state[COMPRESSED_STATE_LAST_UPDATED] = start_time.timestamp() - else: - row_last_updated_ts: float = row.last_updated_ts - comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts - if ( - row_last_changed_ts := row.last_changed_ts - ) and row_last_updated_ts != row_last_changed_ts: - comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_last_changed_ts - return comp_state - - -def decode_attributes_from_row_legacy( - row: Row, attr_cache: dict[str, dict[str, Any]] -) -> dict[str, Any]: - """Decode attributes from a database row.""" - return decode_attributes_from_source( - getattr(row, "shared_attrs", None) or getattr(row, "attributes", None), - attr_cache, - ) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index ea2b93efba7985..6b6c2c2c365ccf 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -116,9 +116,7 @@ def purge_old_data( # This purge cycle is finished, clean up old event types and # recorder runs _purge_old_event_types(instance, session) - - if instance.states_meta_manager.active: - _purge_old_entity_ids(instance, session) + _purge_old_entity_ids(instance, session) _purge_old_recorder_runs(instance, session, purge_before) with session_scope(session=instance.get_session(), read_only=True) as session: diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index 75afb6589a14ae..0ea2c7415b9d1f 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -24,8 +24,6 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): """Manage the StatesMeta table.""" - active = True - def __init__(self, recorder: Recorder) -> None: """Initialize the states meta manager.""" self._did_first_load = False diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9876167e515796..53beb6b43c2121 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -110,9 +110,7 @@ def _simple_version(version: str) -> AwesomeVersion: DAYS_IN_WEEK = 7 -def execute( - qry: Query, to_native: bool = False, validate_entity_ids: bool = True -) -> list[Row]: +def execute(qry: Query) -> list[Row]: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. @@ -122,33 +120,15 @@ def execute( try: if debug: timer_start = time.perf_counter() - - if to_native: - result = [ - row - for row in ( - row.to_native(validate_entity_id=validate_entity_ids) - for row in qry - ) - if row is not None - ] - else: - result = qry.all() + result = qry.all() if debug: elapsed = time.perf_counter() - timer_start - if to_native: - _LOGGER.debug( - "converting %d rows to native objects took %fs", - len(result), - elapsed, - ) - else: - _LOGGER.debug( - "querying %d rows took %fs", - len(result), - elapsed, - ) + _LOGGER.debug( + "querying %d rows took %fs", + len(result), + elapsed, + ) except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 0eff2287a73e1b..ae8cb682c417a9 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -377,8 +377,10 @@ "max": "Max", "high": "[%key:common::state::high%]", "intense": "Intense", + "extreme": "Extreme", "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", "custom_water_flow": "Custom water flow", + "vac_followed_by_mop": "Vacuum followed by mop", "smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]" } }, diff --git a/homeassistant/components/togrill/coordinator.py b/homeassistant/components/togrill/coordinator.py index dda20500235816..c06888eefb0f18 100644 --- a/homeassistant/components/togrill/coordinator.py +++ b/homeassistant/components/togrill/coordinator.py @@ -5,7 +5,6 @@ from collections.abc import Callable from datetime import timedelta import logging -from typing import TypeVar from bleak.exc import BleakError from togrill_bluetooth.client import Client @@ -39,8 +38,6 @@ SCAN_INTERVAL = timedelta(seconds=30) LOGGER = logging.getLogger(__name__) -PacketType = TypeVar("PacketType", bound=Packet) - def get_version_string(packet: PacketA0Notify) -> str: """Construct a version string from packet data.""" @@ -179,9 +176,9 @@ async def _get_connected_client(self) -> Client: self.client = await self._connect_and_update_registry() return self.client - def get_packet( - self, packet_type: type[PacketType], probe=None - ) -> PacketType | None: + def get_packet[PacketT: Packet]( + self, packet_type: type[PacketT], probe=None + ) -> PacketT | None: """Get a cached packet of a certain type.""" if packet := self.data.get((packet_type.type, probe)): diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index fcae7793185f63..e1941250b64434 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -16,7 +16,7 @@ import re import secrets from time import monotonic -from typing import Any, Final, Generic, Protocol, TypeVar +from typing import Any, Final, Protocol from aiohttp import web import mutagen @@ -628,10 +628,7 @@ class HasLastUsed(Protocol): last_used: float -T = TypeVar("T", bound=HasLastUsed) - - -class DictCleaning(Generic[T]): +class DictCleaning[T: HasLastUsed]: """Helper to clean up the stale sessions.""" unsub: CALLBACK_TYPE | None = None diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index cbb4915f4a0c1e..fa4de64e05255a 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, Generic, TypeVar, cast +from typing import Any, cast from volvocarsapi.api import VolvoCarsApi from volvocarsapi.models import ( @@ -64,10 +64,7 @@ def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool: return False -T = TypeVar("T", bound=dict, default=dict[str, Any]) - - -class VolvoBaseCoordinator(DataUpdateCoordinator[T], Generic[T]): +class VolvoBaseCoordinator[T: dict = dict[str, Any]](DataUpdateCoordinator[T]): """Volvo base coordinator.""" config_entry: VolvoConfigEntry diff --git a/homeassistant/components/weatherflow_cloud/coordinator.py b/homeassistant/components/weatherflow_cloud/coordinator.py index ed3f8445110187..94eba6ce5a492c 100644 --- a/homeassistant/components/weatherflow_cloud/coordinator.py +++ b/homeassistant/components/weatherflow_cloud/coordinator.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar from aiohttp import ClientResponseError from weatherflow4py.api import WeatherFlowRestAPI @@ -29,10 +28,8 @@ from .const import DOMAIN, LOGGER -T = TypeVar("T") - -class BaseWeatherFlowCoordinator(DataUpdateCoordinator[dict[int, T]], ABC, Generic[T]): +class BaseWeatherFlowCoordinator[T](DataUpdateCoordinator[dict[int, T]], ABC): """Base class for WeatherFlow coordinators.""" def __init__( @@ -106,9 +103,7 @@ def get_station_name(self, station_id: int) -> str: return self.data[station_id].station.name -class BaseWebsocketCoordinator( - BaseWeatherFlowCoordinator[dict[int, T | None]], ABC, Generic[T] -): +class BaseWebsocketCoordinator[T](BaseWeatherFlowCoordinator[dict[int, T | None]], ABC): """Base class for websocket coordinators.""" _event_type: EventType diff --git a/homeassistant/components/workday/util.py b/homeassistant/components/workday/util.py index 726563febafb9a..9762e1b42fa102 100644 --- a/homeassistant/components/workday/util.py +++ b/homeassistant/components/workday/util.py @@ -1,5 +1,7 @@ """Helpers functions for the Workday component.""" +from __future__ import annotations + from datetime import date, timedelta from functools import partial from typing import TYPE_CHECKING @@ -20,7 +22,7 @@ async def async_validate_country_and_province( hass: HomeAssistant, - entry: "WorkdayConfigEntry", + entry: WorkdayConfigEntry, country: str | None, province: str | None, ) -> None: @@ -180,7 +182,7 @@ def get_holidays_object( def add_remove_custom_holidays( hass: HomeAssistant, - entry: "WorkdayConfigEntry", + entry: WorkdayConfigEntry, country: str | None, calc_add_holidays: list[DateLike], calc_remove_holidays: list[str], diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index a6b45cbd086369..bece865bef21ed 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -5,6 +5,7 @@ from abc import abstractmethod import collections from contextlib import suppress +from enum import StrEnum import json from typing import Any @@ -82,9 +83,6 @@ CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee" -OPTIONS_INTENT_MIGRATE = "intent_migrate" -OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" - UPLOADED_BACKUP_FILE = "uploaded_backup_file" REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/" @@ -102,6 +100,13 @@ ) +class OptionsMigrationIntent(StrEnum): + """Zigbee options flow intents.""" + + MIGRATE = "intent_migrate" + RECONFIGURE = "intent_reconfigure" + + def _format_backup_choice( backup: zigpy.backups.NetworkBackup, *, pan_ids: bool = True ) -> str: @@ -930,6 +935,8 @@ async def _async_create_radio_entry(self) -> ConfigFlowResult: class ZhaOptionsFlowHandler(BaseZhaFlow, OptionsFlow): """Handle an options flow.""" + _migration_intent: OptionsMigrationIntent + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" super().__init__() @@ -971,8 +978,8 @@ async def async_step_prompt_migrate_or_reconfigure( return self.async_show_menu( step_id="prompt_migrate_or_reconfigure", menu_options=[ - OPTIONS_INTENT_RECONFIGURE, - OPTIONS_INTENT_MIGRATE, + OptionsMigrationIntent.RECONFIGURE, + OptionsMigrationIntent.MIGRATE, ], ) @@ -980,30 +987,26 @@ async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Virtual step for when the user is reconfiguring the integration.""" + self._migration_intent = OptionsMigrationIntent.RECONFIGURE return await self.async_step_choose_serial_port() async def async_step_intent_migrate( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm the user wants to reset their current radio.""" + self._migration_intent = OptionsMigrationIntent.MIGRATE + return await self.async_step_choose_serial_port() - if user_input is not None: - await self._radio_mgr.async_reset_adapter() - - return await self.async_step_instruct_unplug() - - return self.async_show_form(step_id="intent_migrate") - - async def async_step_instruct_unplug( + async def async_step_maybe_reset_old_radio( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Instruct the user to unplug the current radio, if possible.""" + """Erase the old radio's network settings before migration.""" - if user_input is not None: - # Now that the old radio is gone, we can scan for serial ports again - return await self.async_step_choose_serial_port() + # If we are reconfiguring, the old radio will not be available + if self._migration_intent is OptionsMigrationIntent.RECONFIGURE: + return await self.async_step_maybe_confirm_ezsp_restore() - return self.async_show_form(step_id="instruct_unplug") + return await super().async_step_maybe_reset_old_radio(user_input) async def _async_create_radio_entry(self): """Re-implementation of the base flow's final step to update the config.""" @@ -1018,8 +1021,7 @@ async def _async_create_radio_entry(self): # Reload ZHA after we finish await self.hass.config_entries.async_setup(self.config_entry.entry_id) - # Intentionally do not set `data` to avoid creating `options`, we set it above - return self.async_create_entry(title=self._title, data={}) + return self.async_abort(reason="reconfigure_successful") def async_remove(self): """Maybe reload ZHA if the flow is aborted.""" diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 71709fdc43dc8d..06ab143b6bdf1b 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -147,10 +147,6 @@ "title": "[%key:component::zha::options::step::prompt_migrate_or_reconfigure::menu_options::intent_migrate%]", "description": "Before plugging in your new adapter, your old adapter needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, - "instruct_unplug": { - "title": "Unplug your old adapter", - "description": "Your old adapter has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new adapter." - }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", "data": { @@ -240,7 +236,8 @@ "cannot_resolve_path": "[%key:component::zha::config::abort::cannot_resolve_path%]", "wrong_firmware_installed": "[%key:component::zha::config::abort::wrong_firmware_installed%]", "cannot_restore_backup": "[%key:component::zha::config::abort::cannot_restore_backup%]", - "cannot_restore_backup_no_ieee_confirm": "[%key:component::zha::config::abort::cannot_restore_backup_no_ieee_confirm%]" + "cannot_restore_backup_no_ieee_confirm": "[%key:component::zha::config::abort::cannot_restore_backup_no_ieee_confirm%]", + "reconfigure_successful": "[%key:component::zha::config::abort::reconfigure_successful%]" } }, "config_panel": { diff --git a/homeassistant/const.py b/homeassistant/const.py index 02daeadf011251..4ae1a73df6bee1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -315,34 +315,6 @@ STATE_OK: Final = "ok" STATE_PROBLEM: Final = "problem" -# #### LOCK STATES #### -# STATE_* below are deprecated as of 2024.10 -# use the LockState enum instead. -_DEPRECATED_STATE_LOCKED: Final = DeprecatedConstant( - "locked", - "LockState.LOCKED", - "2025.10", -) -_DEPRECATED_STATE_UNLOCKED: Final = DeprecatedConstant( - "unlocked", - "LockState.UNLOCKED", - "2025.10", -) -_DEPRECATED_STATE_LOCKING: Final = DeprecatedConstant( - "locking", - "LockState.LOCKING", - "2025.10", -) -_DEPRECATED_STATE_UNLOCKING: Final = DeprecatedConstant( - "unlocking", - "LockState.UNLOCKING", - "2025.10", -) -_DEPRECATED_STATE_JAMMED: Final = DeprecatedConstant( - "jammed", - "LockState.JAMMED", - "2025.10", -) # #### ALARM CONTROL PANEL STATES #### # STATE_ALARM_* below are deprecated as of 2024.11 diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index da5649185ddc78..34fe8d2d2bf0c2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ aiodhcpwatcher==1.2.1 aiodiscover==2.7.1 aiodns==3.5.0 -aiohasupervisor==0.3.3b0 +aiohasupervisor==0.3.3 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 aiohttp==3.12.15 @@ -19,7 +19,7 @@ attrs==25.3.0 audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 -bcrypt==4.3.0 +bcrypt==5.0.0 bleak-retry-connector==4.4.3 bleak==1.0.1 bluetooth-adapters==2.1.0 @@ -29,7 +29,7 @@ cached-ipaddress==0.10.0 certifi>=2021.5.30 ciso8601==2.3.3 cronsim==2.6 -cryptography==45.0.7 +cryptography==46.0.2 dbus-fast==2.44.3 file-read-backwards==2.0.0 fnv-hash-fast==1.5.0 @@ -55,7 +55,7 @@ psutil-home-assistant==0.0.1 PyJWT==2.10.1 pymicro-vad==1.0.1 PyNaCl==1.6.0 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 pyserial==3.5 pyspeex-noise==1.0.2 python-slugify==8.0.4 diff --git a/pyproject.toml b/pyproject.toml index 4ee9024c53fc7a..d33177e12760b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 - "aiohasupervisor==0.3.3b0", + "aiohasupervisor==0.3.3", "aiohttp==3.12.15", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", @@ -40,7 +40,7 @@ dependencies = [ "atomicwrites-homeassistant==1.4.1", "audioop-lts==0.2.1", "awesomeversion==25.5.0", - "bcrypt==4.3.0", + "bcrypt==5.0.0", "certifi>=2021.5.30", "ciso8601==2.3.3", "cronsim==2.6", @@ -57,10 +57,10 @@ dependencies = [ "lru-dict==1.3.0", "PyJWT==2.10.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==45.0.7", + "cryptography==46.0.2", "Pillow==11.3.0", "propcache==0.3.2", - "pyOpenSSL==25.1.0", + "pyOpenSSL==25.3.0", "orjson==3.11.3", "packaging>=23.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index 57a2035cdcb0b4..a1c4900b4007bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.5.0 -aiohasupervisor==0.3.3b0 +aiohasupervisor==0.3.3 aiohttp==3.12.15 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 @@ -17,7 +17,7 @@ attrs==25.3.0 atomicwrites-homeassistant==1.4.1 audioop-lts==0.2.1 awesomeversion==25.5.0 -bcrypt==4.3.0 +bcrypt==5.0.0 certifi>=2021.5.30 ciso8601==2.3.3 cronsim==2.6 @@ -29,10 +29,10 @@ ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 PyJWT==2.10.1 -cryptography==45.0.7 +cryptography==46.0.2 Pillow==11.3.0 propcache==0.3.2 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 orjson==3.11.3 packaging>=23.1 psutil-home-assistant==0.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index eef7754c626399..593be74d597ac3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioguardian==2022.07.0 aioharmony==0.5.3 # homeassistant.components.hassio -aiohasupervisor==0.3.3b0 +aiohasupervisor==0.3.3 # homeassistant.components.home_connect aiohomeconnect==0.20.0 @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.5.3 +airos==0.5.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test.txt b/requirements_test.txt index 658c3ab0a7a1b1..78750341109fb2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -41,13 +41,13 @@ types-croniter==6.0.0.20250809 types-caldav==1.3.0.20250516 types-chardet==0.1.5 types-decorator==5.2.0.20250324 -types-pexpect==4.9.0.20250809 -types-protobuf==6.30.2.20250822 -types-psutil==7.0.0.20250822 -types-pyserial==3.5.0.20250822 +types-pexpect==4.9.0.20250916 +types-protobuf==6.30.2.20250914 +types-psutil==7.0.0.20251001 +types-pyserial==3.5.0.20251001 types-python-dateutil==2.9.0.20250822 types-python-slugify==8.0.2.20240310 types-pytz==2025.2.0.20250809 -types-PyYAML==6.0.12.20250822 -types-requests==2.32.4.20250809 +types-PyYAML==6.0.12.20250915 +types-requests==2.32.4.20250913 types-xmltodict==0.13.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99e8b57e6a07d6..b77ebe6937d7b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioguardian==2022.07.0 aioharmony==0.5.3 # homeassistant.components.hassio -aiohasupervisor==0.3.3b0 +aiohasupervisor==0.3.3 # homeassistant.components.home_connect aiohomeconnect==0.20.0 @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.5.3 +airos==0.5.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/tests/components/airzone_cloud/conftest.py b/tests/components/airzone_cloud/conftest.py index 10388eb63d3b8b..5a94a292c493c4 100644 --- a/tests/components/airzone_cloud/conftest.py +++ b/tests/components/airzone_cloud/conftest.py @@ -9,7 +9,7 @@ class MockAirzoneCloudApi(AirzoneCloudApi): """Mock AirzoneCloudApi class.""" - async def mock_update(self: "AirzoneCloudApi"): + async def mock_update(self): """Mock AirzoneCloudApi _update function.""" await self.update_polling() diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 09aae385a895b4..37627b2f63fdf8 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -3,7 +3,6 @@ from collections.abc import Callable from http import HTTPStatus import io -from types import ModuleType from unittest.mock import ANY, AsyncMock, Mock, PropertyMock, mock_open, patch import pytest @@ -40,11 +39,7 @@ from .common import EMPTY_8_6_JPEG, STREAM_SOURCE, mock_turbo_jpeg -from tests.common import ( - async_fire_time_changed, - help_test_all, - import_and_test_deprecated_constant_enum, -) +from tests.common import async_fire_time_changed from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -807,32 +802,6 @@ async def test_use_stream_for_stills( assert await resp.read() == b"stream_keyframe_image" -@pytest.mark.parametrize( - "module", - [camera], -) -def test_all(module: ModuleType) -> None: - """Test module.__all__ is correctly set.""" - help_test_all(module) - - -@pytest.mark.parametrize( - "enum", - list(camera.const.CameraState), -) -@pytest.mark.parametrize( - "module", - [camera], -) -def test_deprecated_state_constants( - caplog: pytest.LogCaptureFixture, - enum: camera.const.StreamType, - module: ModuleType, -) -> None: - """Test deprecated stream type constants.""" - import_and_test_deprecated_constant_enum(caplog, module, enum, "STATE_", "2025.10") - - @pytest.mark.usefixtures("mock_camera") async def test_entity_picture_url_changes_on_token_update(hass: HomeAssistant) -> None: """Test the token is rotated and entity entity picture cache is cleared.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index cf9c80470499b0..809f57e430958b 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -155,7 +155,7 @@ async def test_camera_stream(hass: HomeAssistant) -> None: ) trt = trait.CameraStreamTrait( - hass, State("camera.bla", camera.STATE_IDLE, {}), BASIC_CONFIG + hass, State("camera.bla", camera.CameraState.IDLE, {}), BASIC_CONFIG ) assert trt.sync_attributes() == { diff --git a/tests/components/history/test_websocket_api_schema_32.py b/tests/components/history/test_websocket_api_schema_32.py deleted file mode 100644 index 8e13f44b822838..00000000000000 --- a/tests/components/history/test_websocket_api_schema_32.py +++ /dev/null @@ -1,162 +0,0 @@ -"""The tests the History component websocket_api.""" - -from collections.abc import Generator - -import pytest - -from homeassistant.components import recorder -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util - -from tests.components.recorder.common import ( - async_recorder_block_till_done, - async_wait_recording_done, - old_db_schema, -) -from tests.typing import WebSocketGenerator - - -@pytest.fixture(autouse=True) -def db_schema_32(hass: HomeAssistant) -> Generator[None]: - """Fixture to initialize the db with the old schema 32.""" - with old_db_schema(hass, "32"): - yield - - -@pytest.mark.usefixtures("recorder_mock") -async def test_history_during_period( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test history_during_period.""" - now = dt_util.utcnow() - - await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - recorder.get_instance(hass).states_meta_manager.active = False - assert recorder.get_instance(hass).schema_version == 32 - - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - await client.send_json( - { - "id": 2, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 2 - - sensor_test_history = response["result"]["sensor.test"] - assert len(sensor_test_history) == 3 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert "a" not in sensor_test_history[1] - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - - assert sensor_test_history[2]["s"] == "on" - assert "a" not in sensor_test_history[2] - - await client.send_json( - { - "id": 3, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 3 - sensor_test_history = response["result"]["sensor.test"] - - assert len(sensor_test_history) == 5 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"any": "attr"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"any": "attr"} - - assert sensor_test_history[4]["s"] == "on" - assert sensor_test_history[4]["a"] == {"any": "attr"} - - await client.send_json( - { - "id": 4, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 4 - sensor_test_history = response["result"]["sensor.test"] - - assert len(sensor_test_history) == 3 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"any": "attr"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"any": "attr"} - - assert sensor_test_history[2]["s"] == "on" - assert sensor_test_history[2]["a"] == {"any": "attr"} diff --git a/tests/components/irm_kmi/conftest.py b/tests/components/irm_kmi/conftest.py index b3ef4fa1b8914f..fe64cdbcd56e25 100644 --- a/tests/components/irm_kmi/conftest.py +++ b/tests/components/irm_kmi/conftest.py @@ -73,7 +73,7 @@ def mock_get_forecast_api_error(): @pytest.fixture -def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock]: +def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked IrmKmi api client.""" fixture: str = "forecast.json" @@ -111,9 +111,7 @@ def mock_irm_kmi_api_high_low_temp(): @pytest.fixture -def mock_exception_irm_kmi_api( - request: pytest.FixtureRequest, -) -> Generator[None, MagicMock]: +def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked IrmKmi api client that will raise an error upon refreshing data.""" with patch( "homeassistant.components.irm_kmi.IrmKmiApiClientHa", autospec=True diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 510034a21725b6..292f1ebd26f3fe 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -2,13 +2,11 @@ from __future__ import annotations -from enum import Enum import re from typing import Any import pytest -from homeassistant.components import lock from homeassistant.components.lock import ( ATTR_CODE, CONF_DEFAULT_CODE, @@ -26,8 +24,6 @@ from .conftest import MockLock -from tests.common import help_test_all, import_and_test_deprecated_constant_enum - async def help_test_async_lock_service( hass: HomeAssistant, @@ -382,38 +378,3 @@ async def test_lock_with_illegal_default_code( == rf"The code for lock.test_lock doesn't match pattern ^\d{{{4}}}$" ) assert exc.value.translation_key == "add_default_code" - - -def test_all() -> None: - """Test module.__all__ is correctly set.""" - help_test_all(lock) - - -def _create_tuples( - enum: type[Enum], constant_prefix: str, remove_in_version: str -) -> list[tuple[Enum, str]]: - return [ - (enum_field, constant_prefix, remove_in_version) - for enum_field in enum - if enum_field - not in [ - lock.LockState.OPEN, - lock.LockState.OPENING, - ] - ] - - -@pytest.mark.parametrize( - ("enum", "constant_prefix", "remove_in_version"), - _create_tuples(lock.LockState, "STATE_", "2025.10"), -) -def test_deprecated_constants( - caplog: pytest.LogCaptureFixture, - enum: Enum, - constant_prefix: str, - remove_in_version: str, -) -> None: - """Test deprecated constants.""" - import_and_test_deprecated_constant_enum( - caplog, lock, enum, constant_prefix, remove_in_version - ) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 552a94e8723147..2d472d0595b3af 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,8 +1,6 @@ """Test the base functions of the media player.""" -from enum import Enum from http import HTTPStatus -from types import ModuleType from unittest.mock import patch import pytest @@ -18,7 +16,6 @@ MediaClass, MediaPlayerEnqueue, MediaPlayerEntity, - MediaPlayerEntityFeature, SearchMedia, SearchMediaQuery, ) @@ -31,11 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import ( - MockEntityPlatform, - help_test_all, - import_and_test_deprecated_constant_enum, -) +from tests.common import MockEntityPlatform from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -46,72 +39,6 @@ async def setup_homeassistant(hass: HomeAssistant): await async_setup_component(hass, "homeassistant", {}) -def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, str]]: - return [ - (enum_field, constant_prefix) - for enum_field in enum - if enum_field - not in [ - MediaPlayerEntityFeature.MEDIA_ANNOUNCE, - MediaPlayerEntityFeature.MEDIA_ENQUEUE, - MediaPlayerEntityFeature.SEARCH_MEDIA, - ] - ] - - -@pytest.mark.parametrize( - "module", - [media_player, media_player.const], -) -def test_all(module: ModuleType) -> None: - """Test module.__all__ is correctly set.""" - help_test_all(module) - - -@pytest.mark.parametrize( - ("enum", "constant_prefix"), - _create_tuples(media_player.MediaPlayerEntityFeature, "SUPPORT_") - + _create_tuples(media_player.MediaPlayerDeviceClass, "DEVICE_CLASS_"), -) -@pytest.mark.parametrize( - "module", - [media_player], -) -def test_deprecated_constants( - 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.10" - ) - - -@pytest.mark.parametrize( - ("enum", "constant_prefix"), - _create_tuples(media_player.MediaClass, "MEDIA_CLASS_") - + _create_tuples(media_player.MediaPlayerEntityFeature, "SUPPORT_") - + _create_tuples(media_player.MediaType, "MEDIA_TYPE_") - + _create_tuples(media_player.RepeatMode, "REPEAT_MODE_"), -) -@pytest.mark.parametrize( - "module", - [media_player.const], -) -def test_deprecated_constants_const( - 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.10" - ) - - @pytest.mark.parametrize( "property_suffix", [ diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index d381c225275198..d1e33d3a62699f 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -28,18 +28,25 @@ statistics, ) from homeassistant.components.recorder.db_schema import ( + EventData, Events, EventTypes, RecorderRuns, + StateAttributes, States, StatesMeta, ) +from homeassistant.components.recorder.models import ( + bytes_to_ulid_or_none, + bytes_to_uuid_hex_or_none, +) from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import DEGREE, UnitOfTemperature from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import recorder as recorder_helper from homeassistant.util import dt as dt_util +from homeassistant.util.json import json_loads, json_loads_object from . import db_schema_0 @@ -492,3 +499,74 @@ def _mock_setup_recorder_connection(): ) await instance.async_add_executor_job(_mock_setup_recorder_connection) + + +EVENT_ORIGIN_ORDER = [ha.EventOrigin.local, ha.EventOrigin.remote] + + +def db_event_to_native(event: Events, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = ha.Context( + id=bytes_to_ulid_or_none(event.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(event.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(event.context_parent_id_bin), + ) + return Event( + event.event_type or "", + json_loads_object(event.event_data) if event.event_data else {}, + ha.EventOrigin(event.origin) + if event.origin + else EVENT_ORIGIN_ORDER[event.origin_idx or 0], + event.time_fired_ts or 0, + context=context, + ) + + +def db_event_data_to_native(event_data: EventData) -> dict[str, Any]: + """Convert to an event data dictionary.""" + shared_data = event_data.shared_data + if shared_data is None: + return {} + return cast(dict[str, Any], json_loads(shared_data)) + + +def db_state_to_native(state: States, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = ha.Context( + id=bytes_to_ulid_or_none(state.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(state.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(state.context_parent_id_bin), + ) + attrs = json_loads_object(state.attributes) if state.attributes else {} + last_updated = dt_util.utc_from_timestamp(state.last_updated_ts or 0) + if state.last_changed_ts is None or state.last_changed_ts == state.last_updated_ts: + last_changed = dt_util.utc_from_timestamp(state.last_updated_ts or 0) + else: + last_changed = dt_util.utc_from_timestamp(state.last_changed_ts or 0) + if ( + state.last_reported_ts is None + or state.last_reported_ts == state.last_updated_ts + ): + last_reported = dt_util.utc_from_timestamp(state.last_updated_ts or 0) + else: + last_reported = dt_util.utc_from_timestamp(state.last_reported_ts or 0) + return State( + state.entity_id or "", + state.state, # type: ignore[arg-type] + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +def db_state_attributes_to_native(state_attrs: StateAttributes) -> dict[str, Any]: + """Convert to a state attributes dictionary.""" + shared_attrs = state_attrs.shared_attrs + if shared_attrs is None: + return {} + return cast(dict[str, Any], json_loads(shared_attrs)) diff --git a/tests/components/recorder/db_schema_32.py b/tests/components/recorder/db_schema_32.py index 9c19a1c74058b6..cf49a3f5e973a5 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -414,10 +414,9 @@ def __repr__(self) -> str: @staticmethod def from_event(event: Event) -> States: """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] state: State | None = event.data.get("new_state") dbstate = States( - entity_id=entity_id, + entity_id=None, attributes=None, context_id=event.context.id, context_user_id=event.context.user_id, diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py index a5381d633cba8a..b0cdecd88dc955 100644 --- a/tests/components/recorder/db_schema_42.py +++ b/tests/components/recorder/db_schema_42.py @@ -488,10 +488,9 @@ def _last_updated_isotime(self) -> str | None: @staticmethod def from_event(event: Event) -> States: """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] state: State | None = event.data.get("new_state") dbstate = States( - entity_id=entity_id, + entity_id=None, attributes=None, context_id=None, context_id_bin=ulid_to_bytes_or_none(event.context.id), diff --git a/tests/components/recorder/db_schema_43.py b/tests/components/recorder/db_schema_43.py index 379e6fbd41644f..31e837d6bb6304 100644 --- a/tests/components/recorder/db_schema_43.py +++ b/tests/components/recorder/db_schema_43.py @@ -519,7 +519,7 @@ def from_event(event: Event[EventStateChangedData]) -> States: context = event.context return States( state=state_value, - entity_id=event.data["entity_id"], + entity_id=None, attributes=None, context_id=None, context_id_bin=ulid_to_bytes_or_none(context.id), diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py deleted file mode 100644 index aa0dcddcf9d0aa..00000000000000 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ /dev/null @@ -1,693 +0,0 @@ -"""The tests for the recorder filter matching the EntityFilter component.""" - -from collections.abc import AsyncGenerator, Generator -import json -from unittest.mock import patch - -import pytest -from sqlalchemy import select -from sqlalchemy.engine.row import Row - -from homeassistant.components.recorder import Recorder, get_instance -from homeassistant.components.recorder.db_schema import EventData, Events, States -from homeassistant.components.recorder.filters import ( - Filters, - extract_include_exclude_filter_conf, - sqlalchemy_filter_from_include_exclude_conf, -) -from homeassistant.components.recorder.util import session_scope -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_DOMAINS, - CONF_ENTITIES, - CONF_EXCLUDE, - CONF_INCLUDE, - STATE_ON, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entityfilter import ( - CONF_ENTITY_GLOBS, - convert_include_exclude_filter, -) - -from .common import async_wait_recording_done, old_db_schema - -from tests.typing import RecorderInstanceContextManager - - -@pytest.fixture -async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceContextManager, -) -> None: - """Set up recorder.""" - - -# This test is for schema 37 and below (32 is new enough to test) -@pytest.fixture(autouse=True) -def db_schema_32(hass: HomeAssistant) -> Generator[None]: - """Fixture to initialize the db with the old schema 32.""" - with old_db_schema(hass, "32"): - yield - - -@pytest.fixture(name="legacy_recorder_mock") -async def legacy_recorder_mock_fixture( - recorder_mock: Recorder, -) -> AsyncGenerator[Recorder]: - """Fixture for legacy recorder mock.""" - with patch.object(recorder_mock.states_meta_manager, "active", False): - yield recorder_mock - - -async def _async_get_states_and_events_with_filter( - hass: HomeAssistant, sqlalchemy_filter: Filters, entity_ids: set[str] -) -> tuple[list[Row], list[Row]]: - """Get states from the database based on a filter.""" - for entity_id in entity_ids: - hass.states.async_set(entity_id, STATE_ON) - hass.bus.async_fire("any", {ATTR_ENTITY_ID: entity_id}) - - await async_wait_recording_done(hass) - - def _get_states_with_session(): - with session_scope(hass=hass) as session: - return session.execute( - select(States.entity_id).filter( - sqlalchemy_filter.states_entity_filter() - ) - ).all() - - filtered_states_entity_ids = { - row[0] - for row in await get_instance(hass).async_add_executor_job( - _get_states_with_session - ) - } - - def _get_events_with_session(): - with session_scope(hass=hass) as session: - return session.execute( - select(EventData.shared_data) - .outerjoin(Events, EventData.data_id == Events.data_id) - .filter(sqlalchemy_filter.events_entity_filter()) - ).all() - - filtered_events_entity_ids = set() - for row in await get_instance(hass).async_add_executor_job( - _get_events_with_session - ): - event_data = json.loads(row[0]) - if ATTR_ENTITY_ID not in event_data: - continue - filtered_events_entity_ids.add(json.loads(row[0])[ATTR_ENTITY_ID]) - - return filtered_states_entity_ids, filtered_events_entity_ids - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_and_excluded_simple_case_no_domains( - hass: HomeAssistant, -) -> None: - """Test filters with included and excluded without domains.""" - filter_accept = {"sensor.kitchen4", "switch.kitchen"} - filter_reject = { - "light.any", - "switch.other", - "cover.any", - "sensor.weather5", - "light.kitchen", - } - conf = { - CONF_INCLUDE: { - CONF_ENTITY_GLOBS: ["sensor.kitchen*"], - CONF_ENTITIES: ["switch.kitchen"], - }, - CONF_EXCLUDE: { - CONF_ENTITY_GLOBS: ["sensor.weather*"], - CONF_ENTITIES: ["light.kitchen"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - assert not entity_filter.explicitly_included("light.any") - assert not entity_filter.explicitly_included("switch.other") - assert entity_filter.explicitly_included("sensor.kitchen4") - assert entity_filter.explicitly_included("switch.kitchen") - - assert not entity_filter.explicitly_excluded("light.any") - assert not entity_filter.explicitly_excluded("switch.other") - assert entity_filter.explicitly_excluded("sensor.weather5") - assert entity_filter.explicitly_excluded("light.kitchen") - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_and_excluded_simple_case_no_globs(hass: HomeAssistant) -> None: - """Test filters with included and excluded without globs.""" - filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"} - filter_reject = {"sensor.bli"} - conf = { - CONF_INCLUDE: { - CONF_DOMAINS: ["sensor", "homeassistant"], - CONF_ENTITIES: ["switch.bla"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["switch"], - CONF_ENTITIES: ["sensor.bli"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_and_excluded_simple_case_without_underscores( - hass: HomeAssistant, -) -> None: - """Test filters with included and excluded without underscores.""" - filter_accept = {"light.any", "sensor.kitchen4", "switch.kitchen"} - filter_reject = {"switch.other", "cover.any", "sensor.weather5", "light.kitchen"} - conf = { - CONF_INCLUDE: { - CONF_DOMAINS: ["light"], - CONF_ENTITY_GLOBS: ["sensor.kitchen*"], - CONF_ENTITIES: ["switch.kitchen"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["cover"], - CONF_ENTITY_GLOBS: ["sensor.weather*"], - CONF_ENTITIES: ["light.kitchen"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - assert not entity_filter.explicitly_included("light.any") - assert not entity_filter.explicitly_included("switch.other") - assert entity_filter.explicitly_included("sensor.kitchen4") - assert entity_filter.explicitly_included("switch.kitchen") - - assert not entity_filter.explicitly_excluded("light.any") - assert not entity_filter.explicitly_excluded("switch.other") - assert entity_filter.explicitly_excluded("sensor.weather5") - assert entity_filter.explicitly_excluded("light.kitchen") - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_and_excluded_simple_case_with_underscores( - hass: HomeAssistant, -) -> None: - """Test filters with included and excluded with underscores.""" - filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} - filter_reject = {"switch.other", "cover.any", "sensor.weather_5", "light.kitchen"} - conf = { - CONF_INCLUDE: { - CONF_DOMAINS: ["light"], - CONF_ENTITY_GLOBS: ["sensor.kitchen_*"], - CONF_ENTITIES: ["switch.kitchen"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["cover"], - CONF_ENTITY_GLOBS: ["sensor.weather_*"], - CONF_ENTITIES: ["light.kitchen"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - assert not entity_filter.explicitly_included("light.any") - assert not entity_filter.explicitly_included("switch.other") - assert entity_filter.explicitly_included("sensor.kitchen_4") - assert entity_filter.explicitly_included("switch.kitchen") - - assert not entity_filter.explicitly_excluded("light.any") - assert not entity_filter.explicitly_excluded("switch.other") - assert entity_filter.explicitly_excluded("sensor.weather_5") - assert entity_filter.explicitly_excluded("light.kitchen") - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_and_excluded_complex_case(hass: HomeAssistant) -> None: - """Test filters with included and excluded with a complex filter.""" - filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} - filter_reject = { - "camera.one", - "notify.any", - "automation.update_readme", - "automation.update_utilities_cost", - "binary_sensor.iss", - } - conf = { - CONF_INCLUDE: { - CONF_ENTITIES: ["group.trackers"], - }, - CONF_EXCLUDE: { - CONF_ENTITIES: [ - "automation.update_readme", - "automation.update_utilities_cost", - "binary_sensor.iss", - ], - CONF_DOMAINS: [ - "camera", - "group", - "media_player", - "notify", - "scene", - "sun", - "zone", - ], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_included_entities_and_excluded_domain(hass: HomeAssistant) -> None: - """Test filters with included entities and excluded domain.""" - filter_accept = { - "media_player.test", - "media_player.test3", - "thermostat.test", - "zone.home", - "script.can_cancel_this_one", - } - filter_reject = { - "thermostat.test2", - } - conf = { - CONF_INCLUDE: { - CONF_ENTITIES: ["media_player.test", "thermostat.test"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["thermostat"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_same_domain_included_excluded(hass: HomeAssistant) -> None: - """Test filters with the same domain included and excluded.""" - filter_accept = { - "media_player.test", - "media_player.test3", - } - filter_reject = { - "thermostat.test2", - "thermostat.test", - "zone.home", - "script.can_cancel_this_one", - } - conf = { - CONF_INCLUDE: { - CONF_DOMAINS: ["media_player"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["media_player"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_same_entity_included_excluded(hass: HomeAssistant) -> None: - """Test filters with the same entity included and excluded.""" - filter_accept = { - "media_player.test", - } - filter_reject = { - "media_player.test3", - "thermostat.test2", - "thermostat.test", - "zone.home", - "script.can_cancel_this_one", - } - conf = { - CONF_INCLUDE: { - CONF_ENTITIES: ["media_player.test"], - }, - CONF_EXCLUDE: { - CONF_ENTITIES: ["media_player.test"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_same_entity_included_excluded_include_domain_wins( - hass: HomeAssistant, -) -> None: - """Test filters with domain and entities and the include domain wins.""" - filter_accept = { - "media_player.test2", - "media_player.test3", - "thermostat.test", - } - filter_reject = { - "thermostat.test2", - "zone.home", - "script.can_cancel_this_one", - } - conf = { - CONF_INCLUDE: { - CONF_DOMAINS: ["media_player"], - CONF_ENTITIES: ["thermostat.test"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["thermostat"], - CONF_ENTITIES: ["media_player.test"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_specificly_included_entity_always_wins(hass: HomeAssistant) -> None: - """Test specifically included entity always wins.""" - filter_accept = { - "media_player.test2", - "media_player.test3", - "thermostat.test", - "binary_sensor.specific_include", - } - filter_reject = { - "binary_sensor.test2", - "binary_sensor.home", - "binary_sensor.can_cancel_this_one", - } - conf = { - CONF_INCLUDE: { - CONF_ENTITIES: ["binary_sensor.specific_include"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["binary_sensor"], - CONF_ENTITY_GLOBS: ["binary_sensor.*"], - }, - } - - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) - - -@pytest.mark.usefixtures("legacy_recorder_mock") -async def test_specificly_included_entity_always_wins_over_glob( - hass: HomeAssistant, -) -> None: - """Test specifically included entity always wins over a glob.""" - filter_accept = { - "sensor.apc900va_status", - "sensor.apc900va_battery_charge", - "sensor.apc900va_battery_runtime", - "sensor.apc900va_load", - "sensor.energy_x", - } - filter_reject = { - "sensor.apc900va_not_included", - } - conf = { - CONF_EXCLUDE: { - CONF_DOMAINS: [ - "updater", - "camera", - "group", - "media_player", - "script", - "sun", - "automation", - "zone", - "weblink", - "scene", - "calendar", - "weather", - "remote", - "notify", - "switch", - "shell_command", - "media_player", - ], - CONF_ENTITY_GLOBS: ["sensor.apc900va_*"], - }, - CONF_INCLUDE: { - CONF_DOMAINS: [ - "binary_sensor", - "climate", - "device_tracker", - "input_boolean", - "sensor", - ], - CONF_ENTITY_GLOBS: ["sensor.energy_*"], - CONF_ENTITIES: [ - "sensor.apc900va_status", - "sensor.apc900va_battery_charge", - "sensor.apc900va_battery_runtime", - "sensor.apc900va_load", - ], - }, - } - extracted_filter = extract_include_exclude_filter_conf(conf) - entity_filter = convert_include_exclude_filter(extracted_filter) - sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) - assert sqlalchemy_filter is not None - - for entity_id in filter_accept: - assert entity_filter(entity_id) is True - - for entity_id in filter_reject: - assert entity_filter(entity_id) is False - - ( - filtered_states_entity_ids, - filtered_events_entity_ids, - ) = await _async_get_states_and_events_with_filter( - hass, sqlalchemy_filter, filter_accept | filter_reject - ) - - assert filtered_states_entity_ids == filter_accept - assert not filtered_states_entity_ids.intersection(filter_reject) - - assert filtered_events_entity_ids == filter_accept - assert not filtered_events_entity_ids.intersection(filter_reject) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index d6223eb55b324c..645d9cede84789 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -32,6 +32,8 @@ assert_states_equal_without_context, async_recorder_block_till_done, async_wait_recording_done, + db_state_attributes_to_native, + db_state_to_native, ) from tests.typing import RecorderInstanceContextManager @@ -884,10 +886,10 @@ def _fetch_native_states() -> list[State]: db_state.entity_id = metadata_id_to_entity_id[ db_state.metadata_id ].entity_id - state = db_state.to_native() - state.attributes = db_state_attributes[ - db_state.attributes_id - ].to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native( + db_state_attributes[db_state.attributes_id] + ) native_states.append(state) return native_states diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py deleted file mode 100644 index 908a67cd635084..00000000000000 --- a/tests/components/recorder/test_history_db_schema_32.py +++ /dev/null @@ -1,737 +0,0 @@ -"""The tests the History component.""" - -from __future__ import annotations - -from collections.abc import Generator -from copy import copy -from datetime import datetime, timedelta -import json -from unittest.mock import patch, sentinel - -from freezegun import freeze_time -import pytest - -from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, history -from homeassistant.components.recorder.filters import Filters -from homeassistant.components.recorder.models import process_timestamp -from homeassistant.components.recorder.util import session_scope -from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.json import JSONEncoder -from homeassistant.util import dt as dt_util - -from .common import ( - assert_dict_of_states_equal_without_context_and_last_changed, - assert_multiple_states_equal_without_context, - assert_multiple_states_equal_without_context_and_last_changed, - assert_states_equal_without_context, - async_wait_recording_done, - old_db_schema, -) - -from tests.typing import RecorderInstanceContextManager - - -@pytest.fixture -async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceContextManager, -) -> None: - """Set up recorder.""" - - -@pytest.fixture -def disable_states_meta_manager(): - """Disable the states meta manager.""" - with patch.object( - recorder.table_managers.states_meta.StatesMetaManager, - "active", - False, - ): - yield - - -@pytest.fixture(autouse=True) -def db_schema_32(hass: HomeAssistant) -> Generator[None]: - """Fixture to initialize the db with the old schema 32.""" - with old_db_schema(hass, "32"): - yield - - -@pytest.fixture(autouse=True) -def setup_recorder( - db_schema_32, disable_states_meta_manager, recorder_mock: Recorder -) -> recorder.Recorder: - """Set up recorder.""" - - -async def test_get_full_significant_states_with_session_entity_no_matches( - hass: HomeAssistant, -) -> None: - """Test getting states at a specific point in time for entities that never have been recorded.""" - now = dt_util.utcnow() - time_before_recorder_ran = now - timedelta(days=1000) - instance = recorder.get_instance(hass) - with ( - session_scope(hass=hass) as session, - patch.object(instance.states_meta_manager, "active", False), - ): - assert ( - history.get_full_significant_states_with_session( - hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] - ) - == {} - ) - assert ( - history.get_full_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id", "demo.id2"], - ) - == {} - ) - - -async def test_significant_states_with_session_entity_minimal_response_no_matches( - hass: HomeAssistant, -) -> None: - """Test getting states at a specific point in time for entities that never have been recorded.""" - now = dt_util.utcnow() - time_before_recorder_ran = now - timedelta(days=1000) - instance = recorder.get_instance(hass) - with ( - session_scope(hass=hass) as session, - patch.object(instance.states_meta_manager, "active", False), - ): - assert ( - history.get_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id"], - minimal_response=True, - ) - == {} - ) - assert ( - history.get_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id", "demo.id2"], - minimal_response=True, - ) - == {} - ) - - -@pytest.mark.parametrize( - ("attributes", "no_attributes", "limit"), - [ - ({"attr": True}, False, 5000), - ({}, True, 5000), - ({"attr": True}, False, 3), - ({}, True, 3), - ], -) -async def test_state_changes_during_period( - hass: HomeAssistant, attributes, no_attributes, limit -) -> None: - """Test state change during period.""" - entity_id = "media_player.test" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state, attributes) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - point = start + timedelta(seconds=1) - end = point + timedelta(seconds=1) - - with freeze_time(start) as freezer: - set_state("idle") - set_state("YouTube") - - freezer.move_to(point) - states = [ - set_state("idle"), - set_state("Netflix"), - set_state("Plex"), - set_state("YouTube"), - ] - - freezer.move_to(end) - set_state("Netflix") - set_state("Plex") - await async_wait_recording_done(hass) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes, limit=limit - ) - - assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) - - -async def test_state_changes_during_period_descending( - hass: HomeAssistant, -) -> None: - """Test state change during period descending.""" - entity_id = "media_player.test" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state, {"any": 1}) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - point = start + timedelta(seconds=1) - point2 = start + timedelta(seconds=1, microseconds=2) - point3 = start + timedelta(seconds=1, microseconds=3) - point4 = start + timedelta(seconds=1, microseconds=4) - end = point + timedelta(seconds=1) - - with freeze_time(start) as freezer: - set_state("idle") - set_state("YouTube") - - freezer.move_to(point) - states = [set_state("idle")] - - freezer.move_to(point2) - states.append(set_state("Netflix")) - - freezer.move_to(point3) - states.append(set_state("Plex")) - - freezer.move_to(point4) - states.append(set_state("YouTube")) - - freezer.move_to(end) - set_state("Netflix") - set_state("Plex") - await async_wait_recording_done(hass) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes=False, descending=False - ) - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes=False, descending=True - ) - assert_multiple_states_equal_without_context( - states, list(reversed(list(hist[entity_id]))) - ) - - -async def test_get_last_state_changes(hass: HomeAssistant) -> None: - """Test number of state changes.""" - entity_id = "sensor.test" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1, seconds=1) - states = [] - - with freeze_time(start) as freezer: - set_state("1") - - freezer.move_to(point) - states.append(set_state("2")) - - freezer.move_to(point2) - states.append(set_state("3")) - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - -async def test_ensure_state_can_be_copied( - hass: HomeAssistant, -) -> None: - """Ensure a state can pass though copy(). - - The filter integration uses copy() on states - from history. - """ - entity_id = "sensor.test" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - - with freeze_time(start) as freezer: - set_state("1") - - freezer.move_to(point) - set_state("2") - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert_states_equal_without_context( - copy(hist[entity_id][0]), hist[entity_id][0] - ) - assert_states_equal_without_context( - copy(hist[entity_id][1]), hist[entity_id][1] - ) - - -async def test_get_significant_states(hass: HomeAssistant) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - hist = history.get_significant_states(hass, zero, four, entity_ids=list(states)) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_minimal_response( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned. - - When minimal responses is set only the first and - last states return a complete state. - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, zero, four, minimal_response=True, entity_ids=list(states) - ) - entites_with_reducable_states = [ - "media_player.test", - "media_player.test3", - ] - - # All states for media_player.test state are reduced - # down to last_changed and state when minimal_response - # is set except for the first state. - # is set. We use JSONEncoder to make sure that are - # pre-encoded last_changed is always the same as what - # will happen with encoding a native state - for entity_id in entites_with_reducable_states: - entity_states = states[entity_id] - for state_idx in range(1, len(entity_states)): - input_state = entity_states[state_idx] - orig_last_changed = json.dumps( - process_timestamp(input_state.last_changed), - cls=JSONEncoder, - ).replace('"', "") - orig_state = input_state.state - entity_states[state_idx] = { - "last_changed": orig_last_changed, - "state": orig_state, - } - - assert len(hist) == len(states) - assert_states_equal_without_context( - states["media_player.test"][0], hist["media_player.test"][0] - ) - assert states["media_player.test"][1] == hist["media_player.test"][1] - assert states["media_player.test"][2] == hist["media_player.test"][2] - - assert_multiple_states_equal_without_context( - states["media_player.test2"], hist["media_player.test2"] - ) - assert_states_equal_without_context( - states["media_player.test3"][0], hist["media_player.test3"][0] - ) - assert states["media_player.test3"][1] == hist["media_player.test3"][1] - - assert_multiple_states_equal_without_context( - states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test"], hist["thermostat.test"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test2"], hist["thermostat.test2"] - ) - - -@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) -async def test_get_significant_states_with_initial( - time_zone, hass: HomeAssistant -) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - await hass.config.async_set_time_zone(time_zone) - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - one = zero + timedelta(seconds=1) - one_and_half = zero + timedelta(seconds=1.5) - for entity_id in states: - if entity_id == "media_player.test": - states[entity_id] = states[entity_id][1:] - for state in states[entity_id]: - if state.last_changed == one: - state.last_changed = one_and_half - - hist = history.get_significant_states( - hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states) - ) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_without_initial( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - one = zero + timedelta(seconds=1) - one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) - one_and_half = zero + timedelta(seconds=1.5) - for entity_id in states: - states[entity_id] = [ - s - for s in states[entity_id] - if s.last_changed not in (one, one_with_microsecond) - ] - del states["media_player.test2"] - - hist = history.get_significant_states( - hass, - one_and_half, - four, - include_start_time_state=False, - entity_ids=list(states), - ) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_entity_id( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned for one entity.""" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test"] - del states["thermostat.test2"] - del states["script.can_cancel_this_one"] - - hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_multiple_entity_ids( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned for one entity.""" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test2"] - del states["script.can_cancel_this_one"] - - hist = history.get_significant_states( - hass, - zero, - four, - ["media_player.test", "thermostat.test"], - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["media_player.test"], hist["media_player.test"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test"], hist["thermostat.test"] - ) - - -async def test_get_significant_states_are_ordered( - hass: HomeAssistant, -) -> None: - """Test order of results from get_significant_states. - - When entity ids are given, the results should be returned with the data - in the same order. - """ - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - zero, four, _states = record_states(hass) - await async_wait_recording_done(hass) - - entity_ids = ["media_player.test", "media_player.test2"] - hist = history.get_significant_states(hass, zero, four, entity_ids) - assert list(hist.keys()) == entity_ids - entity_ids = ["media_player.test2", "media_player.test"] - hist = history.get_significant_states(hass, zero, four, entity_ids) - assert list(hist.keys()) == entity_ids - - -async def test_get_significant_states_only( - hass: HomeAssistant, -) -> None: - """Test significant states when significant_states_only is set.""" - entity_id = "sensor.test" - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - - def set_state(state, **kwargs): - """Set the state.""" - hass.states.async_set(entity_id, state, **kwargs) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=4) - points = [start + timedelta(minutes=i) for i in range(1, 4)] - - states = [] - with freeze_time(start) as freezer: - set_state("123", attributes={"attribute": 10.64}) - - freezer.move_to(points[0]) - # Attributes are different, state not - states.append(set_state("123", attributes={"attribute": 21.42})) - - freezer.move_to(points[1]) - # state is different, attributes not - states.append(set_state("32", attributes={"attribute": 21.42})) - - freezer.move_to(points[2]) - # everything is different - states.append(set_state("412", attributes={"attribute": 54.23})) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, - start, - significant_changes_only=True, - entity_ids=list({state.entity_id for state in states}), - ) - - assert len(hist[entity_id]) == 2 - assert not any( - state.last_updated == states[0].last_updated for state in hist[entity_id] - ) - assert any( - state.last_updated == states[1].last_updated for state in hist[entity_id] - ) - assert any( - state.last_updated == states[2].last_updated for state in hist[entity_id] - ) - - hist = history.get_significant_states( - hass, - start, - significant_changes_only=False, - entity_ids=list({state.entity_id for state in states}), - ) - - assert len(hist[entity_id]) == 3 - assert_multiple_states_equal_without_context_and_last_changed( - states, hist[entity_id] - ) - - -def record_states( - hass: HomeAssistant, -) -> tuple[datetime, datetime, dict[str, list[State]]]: - """Record some test states. - - We inject a bunch of state updates from media player, zone and - thermostat. - """ - mp = "media_player.test" - mp2 = "media_player.test2" - mp3 = "media_player.test3" - therm = "thermostat.test" - therm2 = "thermostat.test2" - zone = "zone.home" - script_c = "script.can_cancel_this_one" - - def set_state(entity_id, state, **kwargs): - """Set the state.""" - hass.states.async_set(entity_id, state, **kwargs) - return hass.states.get(entity_id) - - zero = dt_util.utcnow() - one = zero + timedelta(seconds=1) - two = one + timedelta(seconds=1) - three = two + timedelta(seconds=1) - four = three + timedelta(seconds=1) - - states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} - with freeze_time(one) as freezer: - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp2].append( - set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[mp3].append( - set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[therm].append( - set_state(therm, 20, attributes={"current_temperature": 19.5}) - ) - - freezer.move_to(one + timedelta(microseconds=1)) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - - freezer.move_to(two) - # This state will be skipped only different in time - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) - # This state will be skipped because domain is excluded - set_state(zone, "zoning") - states[script_c].append( - set_state(script_c, "off", attributes={"can_cancel": True}) - ) - states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 19.8}) - ) - states[therm2].append( - set_state(therm2, 20, attributes={"current_temperature": 19}) - ) - - freezer.move_to(three) - states[mp].append( - set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) - ) - states[mp3].append( - set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) - ) - # Attributes changed even though state is the same - states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 20}) - ) - - return zero, four, states - - -async def test_state_changes_during_period_multiple_entities_single_test( - hass: HomeAssistant, -) -> None: - """Test state change during period with multiple entities in the same test. - - This test ensures the sqlalchemy query cache does not - generate incorrect results. - """ - instance = recorder.get_instance(hass) - with patch.object(instance.states_meta_manager, "active", False): - start = dt_util.utcnow() - test_entites = {f"sensor.{i}": str(i) for i in range(30)} - for entity_id, value in test_entites.items(): - hass.states.async_set(entity_id, value) - - await async_wait_recording_done(hass) - end = dt_util.utcnow() - - for entity_id, value in test_entites.items(): - hist = history.state_changes_during_period(hass, start, end, entity_id) - assert len(hist) == 1 - assert hist[entity_id][0].state == value - - -async def test_get_significant_states_without_entity_ids_raises( - hass: HomeAssistant, -) -> None: - """Test at least one entity id is required for get_significant_states.""" - now = dt_util.utcnow() - with pytest.raises(ValueError, match="entity_ids must be provided"): - history.get_significant_states(hass, now, None) - - -async def test_state_changes_during_period_without_entity_ids_raises( - hass: HomeAssistant, -) -> None: - """Test at least one entity id is required for state_changes_during_period.""" - now = dt_util.utcnow() - with pytest.raises(ValueError, match="entity_id must be provided"): - history.state_changes_during_period(hass, now, None) - - -async def test_get_significant_states_with_filters_raises( - hass: HomeAssistant, -) -> None: - """Test passing filters is no longer supported.""" - now = dt_util.utcnow() - with pytest.raises(NotImplementedError, match="Filters are no longer supported"): - history.get_significant_states( - hass, now, None, ["media_player.test"], Filters() - ) - - -async def test_get_significant_states_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test get_significant_states returns an empty dict when entities not in the db.""" - now = dt_util.utcnow() - assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {} - - -async def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test state_changes_during_period returns an empty dict when entities not in the db.""" - now = dt_util.utcnow() - assert ( - history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {} - ) - - -async def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test get_last_state_changes returns an empty dict when entities not in the db.""" - assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {} diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index f00ed177807dad..6f6dbc7dd9c04d 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -90,6 +90,10 @@ async_wait_recording_done, convert_pending_states_to_meta, corrupt_db_file, + db_event_data_to_native, + db_event_to_native, + db_state_attributes_to_native, + db_state_to_native, run_information_with_session, ) @@ -281,8 +285,8 @@ async def test_saving_state(hass: HomeAssistant, setup_recorder: None) -> None: ): db_state.entity_id = states_meta.entity_id db_states.append(db_state) - state = db_state.to_native() - state.attributes = db_state_attributes.to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native(db_state_attributes) assert len(db_states) == 1 assert db_states[0].event_id is None @@ -323,8 +327,8 @@ async def test_saving_state_with_nul( ): db_state.entity_id = states_meta.entity_id db_states.append(db_state) - state = db_state.to_native() - state.attributes = db_state_attributes.to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native(db_state_attributes) assert len(db_states) == 1 assert db_states[0].event_id is None @@ -543,8 +547,8 @@ def event_listener(event): event_data = cast(EventData, event_data) event_types = cast(EventTypes, event_types) - native_event = select_event.to_native() - native_event.data = event_data.to_native() + native_event = db_event_to_native(select_event) + native_event.data = db_event_data_to_native(event_data) native_event.event_type = event_types.event_type events.append(native_event) @@ -599,8 +603,8 @@ async def _add_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[Stat .outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id) ): db_state.entity_id = states_meta.entity_id - native_state = db_state.to_native() - native_state.attributes = db_state_attributes.to_native() + native_state = db_state_to_native(db_state) + native_state.attributes = db_state_attributes_to_native(db_state_attributes) states.append(native_state) convert_pending_states_to_meta(get_instance(hass), session) return states @@ -706,9 +710,9 @@ def _get_events(hass: HomeAssistant, event_type_list: list[str]) -> list[Event]: event_data = cast(EventData, event_data) event_types = cast(EventTypes, event_types) - native_event = event.to_native() + native_event = db_event_to_native(event) if event_data: - native_event.data = event_data.to_native() + native_event.data = db_event_data_to_native(event_data) native_event.event_type = event_types.event_type events.append(native_event) return events @@ -878,8 +882,8 @@ async def test_saving_state_with_oversized_attributes( .outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id) ): db_state.entity_id = states_meta.entity_id - native_state = db_state.to_native() - native_state.attributes = db_state_attributes.to_native() + native_state = db_state_to_native(db_state) + native_state.attributes = db_state_attributes_to_native(db_state_attributes) states.append(native_state) assert "switch.too_big" in caplog.text @@ -1575,8 +1579,8 @@ def event_listener(event): event_data = cast(EventData, event_data) event_types = cast(EventTypes, event_types) - native_event = select_event.to_native() - native_event.data = event_data.to_native() + native_event = db_event_to_native(select_event) + native_event.data = db_event_data_to_native(event_data) native_event.event_type = event_types.event_type db_events.append(native_event) @@ -1626,7 +1630,7 @@ async def test_service_disable_states_not_recording( assert db_states[0].event_id is None db_states[0].entity_id = "test.two" assert ( - db_states[0].to_native().as_dict() + db_state_to_native(db_states[0]).as_dict() == _state_with_context(hass, "test.two").as_dict() ) @@ -1744,7 +1748,7 @@ def _get_last_state(): assert len(db_states) == 1 db_states[0].entity_id = "test.two" assert db_states[0].event_id is None - return db_states[0].to_native() + return db_state_to_native(db_states[0]) state = await instance.async_add_executor_job(_get_last_state) assert state.entity_id == "test.two" @@ -2427,8 +2431,8 @@ class EntityWithExcludedAttributes(MockEntity): ): db_state.entity_id = states_meta.entity_id db_states.append(db_state) - state = db_state.to_native() - state.attributes = db_state_attributes.to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native(db_state_attributes) assert len(db_states) == 1 assert db_states[0].event_id is None @@ -2487,8 +2491,8 @@ class EntityWithExcludedAttributes(MockEntity): ): db_state.entity_id = states_meta.entity_id db_states.append(db_state) - state = db_state.to_native() - state.attributes = db_state_attributes.to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native(db_state_attributes) assert len(db_states) == 1 assert db_states[0].event_id is None @@ -2691,7 +2695,7 @@ def get_events() -> list[Event]: select_event = cast(Events, select_event) event_types = cast(EventTypes, event_types) - native_event = select_event.to_native() + native_event = db_event_to_native(select_event) native_event.event_type = event_types.event_type events.append(native_event) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 035fd9b44408aa..fe0c7454ebd1e4 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -32,7 +32,12 @@ from homeassistant.core import HomeAssistant, State from homeassistant.util import dt as dt_util -from .common import async_wait_recorder, async_wait_recording_done, create_engine_test +from .common import ( + async_wait_recorder, + async_wait_recording_done, + create_engine_test, + db_state_to_native, +) from .conftest import InstrumentedMigration from tests.common import async_fire_time_changed @@ -53,7 +58,7 @@ def _get_native_states(hass: HomeAssistant, entity_id: str) -> list[State]: states = [] for dbstate in session.query(States).filter(States.metadata_id == metadata_id): dbstate.entity_id = entity_id - states.append(dbstate.to_native()) + states.append(db_state_to_native(dbstate)) return states diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 689441260c7b06..24c79b73a4e029 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -21,7 +21,13 @@ from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.exceptions import InvalidEntityFormatError from homeassistant.util import dt as dt_util -from homeassistant.util.json import json_loads +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads + +from .common import ( + db_event_to_native, + db_state_attributes_to_native, + db_state_to_native, +) def test_from_event_to_db_event() -> None: @@ -39,7 +45,7 @@ def test_from_event_to_db_event() -> None: dialect = SupportedDialect.MYSQL db_event.event_data = EventData.shared_data_bytes_from_event(event, dialect) db_event.event_type = event.event_type - assert event.as_dict() == db_event.to_native().as_dict() + assert event.as_dict() == db_event_to_native(db_event).as_dict() def test_from_event_to_db_event_with_null() -> None: @@ -70,7 +76,10 @@ def test_from_event_to_db_state() -> None: {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, context=state.context, ) - assert state.as_dict() == States.from_event(event).to_native().as_dict() + db_state = States.from_event(event) + # Set entity_id, it's set to None by States.from_event + db_state.entity_id = state.entity_id + assert state.as_dict() == db_state_to_native(db_state).as_dict() def test_from_event_to_db_state_attributes() -> None: @@ -88,7 +97,7 @@ def test_from_event_to_db_state_attributes() -> None: db_attrs.shared_attrs = StateAttributes.shared_attrs_bytes_from_event( event, dialect ) - assert db_attrs.to_native() == attrs + assert db_state_attributes_to_native(db_attrs) == attrs def test_from_event_to_db_state_attributes_with_null() -> None: @@ -161,15 +170,13 @@ def test_events_repr_without_timestamp() -> None: assert "2016-07-09 11:00:00+00:00" in repr(events) -def test_handling_broken_json_state_attributes( - caplog: pytest.LogCaptureFixture, -) -> None: +def test_handling_broken_json_state_attributes() -> None: """Test we handle broken json in state attributes.""" state_attributes = StateAttributes( attributes_id=444, hash=1234, shared_attrs="{NOT_PARSE}" ) - assert state_attributes.to_native() == {} - assert "Error converting row to state attributes" in caplog.text + with pytest.raises(JSON_DECODE_EXCEPTIONS): + db_state_attributes_to_native(state_attributes) def test_from_event_to_delete_state() -> None: @@ -184,7 +191,7 @@ def test_from_event_to_delete_state() -> None: ) db_state = States.from_event(event) - assert db_state.entity_id == "sensor.temperature" + assert db_state.entity_id is None assert db_state.state == "" assert db_state.last_changed_ts is None assert db_state.last_updated_ts == pytest.approx(event.time_fired.timestamp()) @@ -196,9 +203,9 @@ def test_states_from_native_invalid_entity_id() -> None: state.entity_id = "test.invalid__id" state.attributes = "{}" with pytest.raises(InvalidEntityFormatError): - state = state.to_native() + state = db_state_to_native(state) - state = state.to_native(validate_entity_id=False) + state = db_state_to_native(state, validate_entity_id=False) assert state.entity_id == "test.invalid__id" @@ -279,10 +286,10 @@ async def test_event_to_db_model() -> None: dialect = SupportedDialect.MYSQL db_event.event_data = EventData.shared_data_bytes_from_event(event, dialect) db_event.event_type = event.event_type - native = db_event.to_native() + native = db_event_to_native(db_event) assert native.as_dict() == event.as_dict() - native = Events.from_event(event).to_native() + native = db_event_to_native(Events.from_event(event)) native.data = ( event.data ) # data is not set by from_event as its in the event_data table diff --git a/tests/components/recorder/test_models_legacy.py b/tests/components/recorder/test_models_legacy.py deleted file mode 100644 index f4cdcd7268b45a..00000000000000 --- a/tests/components/recorder/test_models_legacy.py +++ /dev/null @@ -1,99 +0,0 @@ -"""The tests for the Recorder component legacy models.""" - -from datetime import datetime, timedelta -from unittest.mock import PropertyMock - -import pytest - -from homeassistant.components.recorder.models.legacy import LegacyLazyState -from homeassistant.util import dt as dt_util - - -async def test_legacy_lazy_state_prefers_shared_attrs_over_attrs( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test that the LazyState prefers shared_attrs over attributes.""" - row = PropertyMock( - entity_id="sensor.invalid", - shared_attrs='{"shared":true}', - attributes='{"shared":false}', - ) - assert LegacyLazyState(row, {}, None).attributes == {"shared": True} - - -async def test_legacy_lazy_state_handles_different_last_updated_and_last_changed( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test that the LazyState handles different last_updated and last_changed.""" - now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) - row = PropertyMock( - entity_id="sensor.valid", - state="off", - shared_attrs='{"shared":true}', - last_updated_ts=now.timestamp(), - last_changed_ts=(now - timedelta(seconds=60)).timestamp(), - ) - lstate = LegacyLazyState(row, {}, None) - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2021-06-12T03:03:01.000323+00:00", - "last_updated": "2021-06-12T03:04:01.000323+00:00", - "state": "off", - } - assert lstate.last_updated.timestamp() == row.last_updated_ts - assert lstate.last_changed.timestamp() == row.last_changed_ts - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2021-06-12T03:03:01.000323+00:00", - "last_updated": "2021-06-12T03:04:01.000323+00:00", - "state": "off", - } - - -async def test_legacy_lazy_state_handles_same_last_updated_and_last_changed( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test that the LazyState handles same last_updated and last_changed.""" - now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) - row = PropertyMock( - entity_id="sensor.valid", - state="off", - shared_attrs='{"shared":true}', - last_updated_ts=now.timestamp(), - last_changed_ts=now.timestamp(), - ) - lstate = LegacyLazyState(row, {}, None) - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2021-06-12T03:04:01.000323+00:00", - "last_updated": "2021-06-12T03:04:01.000323+00:00", - "state": "off", - } - assert lstate.last_updated.timestamp() == row.last_updated_ts - assert lstate.last_changed.timestamp() == row.last_changed_ts - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2021-06-12T03:04:01.000323+00:00", - "last_updated": "2021-06-12T03:04:01.000323+00:00", - "state": "off", - } - lstate.last_updated = datetime(2020, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2021-06-12T03:04:01.000323+00:00", - "last_updated": "2020-06-12T03:04:01.000323+00:00", - "state": "off", - } - lstate.last_changed = datetime(2020, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) - assert lstate.as_dict() == { - "attributes": {"shared": True}, - "entity_id": "sensor.valid", - "last_changed": "2020-06-12T03:04:01.000323+00:00", - "last_updated": "2020-06-12T03:04:01.000323+00:00", - "state": "off", - } diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 2bfc2887ab2fc9..38da6ad6c81e67 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -2054,8 +2054,6 @@ async def test_purge_old_states_purges_the_state_metadata_ids( hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old states purges state metadata_ids.""" - assert recorder_mock.states_meta_manager.active is True - utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) eleven_days_ago = utcnow - timedelta(days=11) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 6c324f4b01a0a6..b60db68d7138c0 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -86,18 +86,18 @@ async def test_session_scope_not_setup( async def test_recorder_bad_execute(hass: HomeAssistant, setup_recorder: None) -> None: """Bad execute, retry 3 times.""" - def to_native(validate_entity_id=True): + def _all(): """Raise exception.""" raise SQLAlchemyError mck1 = MagicMock() - mck1.to_native = to_native + mck1.all = _all with ( pytest.raises(SQLAlchemyError), patch("homeassistant.components.recorder.core.time.sleep") as e_mock, ): - util.execute((mck1,), to_native=True) + util.execute(mck1) assert e_mock.call_count == 2 diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 695202b67c81bf..6afce0d3eb5fc8 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -65,6 +65,8 @@ assert_multiple_states_equal_without_context_and_last_changed, async_recorder_block_till_done, async_wait_recording_done, + db_state_attributes_to_native, + db_state_to_native, do_adhoc_statistics, get_start_time, statistics_during_period, @@ -6165,8 +6167,8 @@ def _fetch_states() -> list[State]: .outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id) ): db_state.entity_id = db_states_meta.entity_id - state = db_state.to_native() - state.attributes = db_state_attributes.to_native() + state = db_state_to_native(db_state) + state.attributes = db_state_attributes_to_native(db_state_attributes) native_states.append(state) return native_states diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 3282756fe28285..a3bab79e99dc27 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -65,6 +65,15 @@ properties={ATTR_PROPERTIES_ID: "shelly1pm-AABBCCDDEEFF"}, type="mock_type", ) +DISCOVERY_INFO_WRONG_NAME = ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="Shelly Plus 2PM [DDEEFF]", + port=None, + properties={ATTR_PROPERTIES_ID: "shelly2pm-AABBCCDDEEFF"}, + type="mock_type", +) @pytest.mark.parametrize( @@ -1751,3 +1760,53 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" + + +async def test_zeroconf_wrong_device_name( + hass: HomeAssistant, + mock_rpc_device: Mock, + mock_setup_entry: AsyncMock, + mock_setup: AsyncMock, +) -> None: + """Test zeroconf discovery with mismatched device name.""" + + with patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={ + "mac": "test-mac", + "model": MODEL_PLUS_2PM, + "auth": False, + "gen": 2, + }, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO_WRONG_NAME, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"]["name"] == "Shelly Plus 2PM [DDEEFF]" + assert context["confirm_only"] is True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Test name" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_MODEL: MODEL_PLUS_2PM, + CONF_SLEEP_PERIOD: 0, + CONF_GEN: 2, + } + assert result["result"].unique_id == "test-mac" + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 897050a660388a..5ef914ee0b11b1 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -184,6 +184,7 @@ "qxj_is2indt9nlth6esa", # https://github.com/home-assistant/core/issues/136472 "qxj_xbwbniyt6bgws9ia", # https://github.com/orgs/home-assistant/discussions/823 "rqbj_4iqe2hsfyd86kwwc", # https://github.com/orgs/home-assistant/discussions/100 + "rs_d7woucobqi8ncacf", # https://github.com/orgs/home-assistant/discussions/1021 "sd_i6hyjg3af7doaswm", # https://github.com/orgs/home-assistant/discussions/539 "sd_lr33znaodtyarrrz", # https://github.com/home-assistant/core/issues/141278 "sfkzq_1fcnd8xk", # https://github.com/orgs/home-assistant/discussions/539 diff --git a/tests/components/tuya/fixtures/rs_d7woucobqi8ncacf.json b/tests/components/tuya/fixtures/rs_d7woucobqi8ncacf.json new file mode 100644 index 00000000000000..11953b857b957b --- /dev/null +++ b/tests/components/tuya/fixtures/rs_d7woucobqi8ncacf.json @@ -0,0 +1,57 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Geti Solar PV Water Heater", + "category": "rs", + "product_id": "d7woucobqi8ncacf", + "product_name": "Geti Solar PV Water Heater", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-08-16T10:03:14+00:00", + "create_time": "2025-08-16T10:03:14+00:00", + "update_time": "2025-08-16T10:03:14+00:00", + "function": {}, + "status_range": { + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "power_consumption": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 10000, + "scale": 0, + "step": 1 + } + }, + "fault": { + "type": "bitmap", + "value": { + "label": [ + "pv_voltage_high", + "ac_voltage_high", + "water_temp_high", + "water_temp_unknown" + ] + } + } + }, + "status": { + "temp_current": 60, + "power_consumption": 86, + "fault": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 344f638ddf2a49..2673383f4f2f53 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -515,6 +515,67 @@ 'state': 'heat_cool', }) # --- +# name: test_platform_setup_and_discovery[climate.geti_solar_pv_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.geti_solar_pv_water_heater', + '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': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fcacn8iqbocuow7dsr', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.geti_solar_pv_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 60.0, + 'friendly_name': 'Geti Solar PV Water Heater', + 'hvac_modes': list([ + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + }), + 'context': , + 'entity_id': 'climate.geti_solar_pv_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[climate.kabinet-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 3a586bf8011869..3bb6181d76f300 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -2975,6 +2975,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[fcacn8iqbocuow7dsr] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fcacn8iqbocuow7dsr', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Geti Solar PV Water Heater', + 'model_id': 'd7woucobqi8ncacf', + 'name': 'Geti Solar PV Water Heater', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[fcdadqsiax2gvnt0qld] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index d961e1ed4f0ffb..7a34884d160a67 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -1,7 +1,7 @@ """Tests for the Twitch component.""" from collections.abc import AsyncGenerator, AsyncIterator -from typing import Any, Generic, TypeVar +from typing import Any from twitchAPI.object.base import TwitchObject @@ -20,10 +20,7 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.async_block_till_done() -TwitchType = TypeVar("TwitchType", bound=TwitchObject) - - -class TwitchIterObject(Generic[TwitchType]): +class TwitchIterObject[TwitchT: TwitchObject]: """Twitch object iterator.""" raw_data: JsonArrayType @@ -31,14 +28,14 @@ class TwitchIterObject(Generic[TwitchType]): total: int def __init__( - self, hass: HomeAssistant, fixture: str, target_type: type[TwitchType] + self, hass: HomeAssistant, fixture: str, target_type: type[TwitchT] ) -> None: """Initialize object.""" self.hass = hass self.fixture = fixture self.target_type = target_type - async def __aiter__(self) -> AsyncIterator[TwitchType]: + async def __aiter__(self) -> AsyncIterator[TwitchT]: """Return async iterator.""" if not hasattr(self, "raw_data"): self.raw_data = await async_load_json_array_fixture( @@ -50,18 +47,18 @@ async def __aiter__(self) -> AsyncIterator[TwitchType]: yield item -async def get_generator( - hass: HomeAssistant, fixture: str, target_type: type[TwitchType] -) -> AsyncGenerator[TwitchType]: +async def get_generator[TwitchT: TwitchObject]( + hass: HomeAssistant, fixture: str, target_type: type[TwitchT] +) -> AsyncGenerator[TwitchT]: """Return async generator.""" data = await async_load_json_array_fixture(hass, fixture, DOMAIN) async for item in get_generator_from_data(data, target_type): yield item -async def get_generator_from_data( - items: list[dict[str, Any]], target_type: type[TwitchType] -) -> AsyncGenerator[TwitchType]: +async def get_generator_from_data[TwitchT: TwitchObject]( + items: list[dict[str, Any]], target_type: type[TwitchT] +) -> AsyncGenerator[TwitchT]: """Return async generator.""" for item in items: yield target_type(**item) diff --git a/tests/components/vegehub/conftest.py b/tests/components/vegehub/conftest.py index feae5deccbe3e9..6559de5de31fc7 100644 --- a/tests/components/vegehub/conftest.py +++ b/tests/components/vegehub/conftest.py @@ -41,7 +41,7 @@ @pytest.fixture(autouse=True) -def mock_vegehub() -> Generator[Any, Any, Any]: +def mock_vegehub() -> Generator[Any]: """Mock the VegeHub library.""" with patch( "homeassistant.components.vegehub.config_flow.VegeHub", autospec=True diff --git a/tests/components/vegehub/test_config_flow.py b/tests/components/vegehub/test_config_flow.py index 1cf3924f72fb06..a6061a3a159033 100644 --- a/tests/components/vegehub/test_config_flow.py +++ b/tests/components/vegehub/test_config_flow.py @@ -40,7 +40,7 @@ @pytest.fixture(autouse=True) -def mock_setup_entry() -> Generator[Any, Any, Any]: +def mock_setup_entry() -> Generator[Any]: """Prevent the actual integration from being set up.""" with ( patch("homeassistant.components.vegehub.async_setup_entry", return_value=True), diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 2e186bc39d0ac0..35fe0076cacfba 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -1,5 +1,8 @@ """Test Zeroconf multiple instance protection.""" +from __future__ import annotations + +from typing import Self from unittest.mock import Mock, patch import pytest @@ -20,7 +23,7 @@ class MockZeroconf: def __init__(self, *args, **kwargs) -> None: """Initialize the mock.""" - def __new__(cls, *args, **kwargs) -> "MockZeroconf": + def __new__(cls, *args, **kwargs) -> Self: """Return the shared instance.""" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index ce1b1f92f37962..aae16dbccfba81 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -160,7 +160,7 @@ async def detect(self): def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: """Mock of a serial port.""" - port = ListPortInfo("/dev/ttyUSB1234") + port = ListPortInfo(device) port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = device @@ -2102,7 +2102,7 @@ async def test_options_flow_defaults( assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( flow["flow_id"], - user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + user_input={"next_step_id": config_flow.OptionsMigrationIntent.RECONFIGURE}, ) # Current path is the default @@ -2180,8 +2180,8 @@ async def test_options_flow_defaults( ) await hass.async_block_till_done() - assert result7["type"] is FlowResultType.CREATE_ENTRY - assert result7["data"] == {} + assert result7["type"] is FlowResultType.ABORT + assert result7["reason"] == "reconfigure_successful" # The updated entry contains correct settings assert entry.data == { @@ -2240,7 +2240,7 @@ async def test_options_flow_defaults_socket(hass: HomeAssistant) -> None: assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( flow["flow_id"], - user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + user_input={"next_step_id": config_flow.OptionsMigrationIntent.RECONFIGURE}, ) # Radio path must be manually entered @@ -2320,7 +2320,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled( assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( flow["flow_id"], - user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + user_input={"next_step_id": config_flow.OptionsMigrationIntent.RECONFIGURE}, ) # Radio path must be manually entered @@ -2336,19 +2336,18 @@ async def test_options_flow_restarts_running_zha_if_cancelled( async_setup_entry.assert_called_once_with(hass, entry) -@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_options_flow_migration_reset_old_adapter( - hass: HomeAssistant, mock_app + hass: HomeAssistant, backup, mock_app ) -> None: - """Test options flow for migrating from an old radio.""" + """Test options flow for migrating resets the old radio, not the new one.""" entry = MockConfigEntry( version=config_flow.ZhaConfigFlowHandler.VERSION, domain=DOMAIN, data={ CONF_DEVICE: { - CONF_DEVICE_PATH: "/dev/serial/by-id/old_radio", + CONF_DEVICE_PATH: "/dev/ttyUSB_old", CONF_BAUDRATE: 12345, CONF_FLOW_CONTROL: None, }, @@ -2366,39 +2365,158 @@ async def test_options_flow_migration_reset_old_adapter( with patch( "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True ): - result1 = await hass.config_entries.options.async_configure( + result_init = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) entry.mock_state(hass, ConfigEntryState.NOT_LOADED) - assert result1["step_id"] == "prompt_migrate_or_reconfigure" - result2 = await hass.config_entries.options.async_configure( - flow["flow_id"], - user_input={"next_step_id": config_flow.OPTIONS_INTENT_MIGRATE}, - ) + assert result_init["step_id"] == "prompt_migrate_or_reconfigure" - # User must explicitly approve radio reset - assert result2["step_id"] == "intent_migrate" + with ( + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.RADIO_TYPE_DETECTED, + ), + patch( + "serial.tools.list_ports.comports", + MagicMock(return_value=[com_port("/dev/ttyUSB_new")]), + ), + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._async_read_backups_from_database", + return_value=[backup], + ), + ): + result_migrate = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OptionsMigrationIntent.MIGRATE}, + ) - mock_app.reset_network_info = AsyncMock() + # Now we choose the new radio + assert result_migrate["step_id"] == "choose_serial_port" - result3 = await hass.config_entries.options.async_configure( - flow["flow_id"], - user_input={}, - ) + result_port = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + CONF_DEVICE_PATH: "/dev/ttyUSB_new - Some serial port, s/n: 1234 - Virtual serial port" + }, + ) - mock_app.reset_network_info.assert_awaited_once() + assert result_port["step_id"] == "choose_migration_strategy" - # Now we can unplug the old radio - assert result3["step_id"] == "instruct_unplug" + # A temporary radio manager is created to reset the old adapter + mock_radio_manager = AsyncMock() - # And move on to choosing the new radio - result4 = await hass.config_entries.options.async_configure( - flow["flow_id"], - user_input={}, + with patch( + "homeassistant.components.zha.config_flow.ZhaRadioManager", + spec=ZhaRadioManager, + side_effect=[mock_radio_manager], + ): + result_strategy = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + "next_step_id": config_flow.MIGRATION_STRATEGY_RECOMMENDED, + }, + ) + + # The old adapter is reset, not the new one + assert mock_radio_manager.device_path == "/dev/ttyUSB_old" + assert mock_radio_manager.async_reset_adapter.call_count == 1 + + assert result_strategy["type"] is FlowResultType.ABORT + assert result_strategy["reason"] == "reconfigure_successful" + + # The entry is updated + assert entry.data["device"]["path"] == "/dev/ttyUSB_new" + + +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_options_flow_reconfigure_no_reset( + hass: HomeAssistant, backup, mock_app +) -> None: + """Test options flow for reconfiguring does not require the old adapter.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/ttyUSB_old", + CONF_BAUDRATE: 12345, + CONF_FLOW_CONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, ) - assert result4["step_id"] == "choose_serial_port" + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True + ): + result_init = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) + + assert result_init["step_id"] == "prompt_migrate_or_reconfigure" + + with ( + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.RADIO_TYPE_DETECTED, + ), + patch( + "serial.tools.list_ports.comports", + MagicMock(return_value=[com_port("/dev/ttyUSB_new")]), + ), + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._async_read_backups_from_database", + return_value=[backup], + ), + ): + result_reconfigure = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OptionsMigrationIntent.RECONFIGURE}, + ) + + # Now we choose the new radio + assert result_reconfigure["step_id"] == "choose_serial_port" + + result_port = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + CONF_DEVICE_PATH: "/dev/ttyUSB_new - Some serial port, s/n: 1234 - Virtual serial port" + }, + ) + + assert result_port["step_id"] == "choose_migration_strategy" + + with patch( + "homeassistant.components.zha.config_flow.ZhaRadioManager" + ) as mock_radio_manager: + result_strategy = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + "next_step_id": config_flow.MIGRATION_STRATEGY_RECOMMENDED, + }, + ) + + # A temp radio manager is never created + assert mock_radio_manager.call_count == 0 + + assert result_strategy["type"] is FlowResultType.ABORT + assert result_strategy["reason"] == "reconfigure_successful" + + # The entry is updated + assert entry.data["device"]["path"] == "/dev/ttyUSB_new" @pytest.mark.parametrize( diff --git a/tests/test_const.py b/tests/test_const.py index 3398a571f6f983..4413e8efe9618d 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -8,7 +8,7 @@ import pytest from homeassistant import const -from homeassistant.components import alarm_control_panel, lock +from homeassistant.components import alarm_control_panel from .common import ( extract_stack_to_frame, @@ -52,53 +52,15 @@ def test_deprecated_constant_name_changes( ) -def _create_tuples_lock_states( - enum: type[Enum], constant_prefix: str, remove_in_version: str -) -> list[tuple[Enum, str]]: - return [ - (enum_field, constant_prefix, remove_in_version) - for enum_field in enum - if enum_field - not in [ - lock.LockState.OPEN, - lock.LockState.OPENING, - ] - ] - - -@pytest.mark.parametrize( - ("enum", "constant_prefix", "remove_in_version"), - _create_tuples_lock_states(lock.LockState, "STATE_", "2025.10"), -) -def test_deprecated_constants_lock( - caplog: pytest.LogCaptureFixture, - enum: Enum, - constant_prefix: str, - remove_in_version: str, -) -> None: - """Test deprecated constants.""" - import_and_test_deprecated_constant_enum( - caplog, const, enum, constant_prefix, remove_in_version - ) - - def _create_tuples_alarm_states( enum: type[Enum], constant_prefix: str, remove_in_version: str ) -> list[tuple[Enum, str]]: - return [ - (enum_field, constant_prefix, remove_in_version) - for enum_field in enum - if enum_field - not in [ - lock.LockState.OPEN, - lock.LockState.OPENING, - ] - ] + return [(enum_field, constant_prefix, remove_in_version) for enum_field in enum] @pytest.mark.parametrize( ("enum", "constant_prefix", "remove_in_version"), - _create_tuples_lock_states( + _create_tuples_alarm_states( alarm_control_panel.AlarmControlPanelState, "STATE_ALARM_", "2025.11" ), )