diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 490ce5559a9..58150ae7926 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -716,109 +716,6 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: return domains -class _WatchPendingSetups: - """Periodic log and dispatch of setups that are pending.""" - - def __init__( - self, - hass: core.HomeAssistant, - setup_started: dict[tuple[str, str | None], float], - ) -> None: - """Initialize the WatchPendingSetups class.""" - self._hass = hass - self._setup_started = setup_started - self._duration_count = 0 - self._handle: asyncio.TimerHandle | None = None - self._previous_was_empty = True - self._loop = hass.loop - - def _async_watch(self) -> None: - """Periodic log of setups that are pending.""" - now = monotonic() - self._duration_count += SLOW_STARTUP_CHECK_INTERVAL - - remaining_with_setup_started: defaultdict[str, float] = defaultdict(float) - for integration_group, start_time in self._setup_started.items(): - domain, _ = integration_group - remaining_with_setup_started[domain] += now - start_time - - if remaining_with_setup_started: - _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) - elif waiting_tasks := self._hass._active_tasks: # noqa: SLF001 - _LOGGER.debug("Waiting on tasks: %s", waiting_tasks) - self._async_dispatch(remaining_with_setup_started) - if ( - self._setup_started - and self._duration_count % LOG_SLOW_STARTUP_INTERVAL == 0 - ): - # We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done - # once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up - _LOGGER.warning( - "Waiting on integrations to complete setup: %s", - self._setup_started, - ) - - _LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones) - self._async_schedule_next() - - def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None: - """Dispatch the signal.""" - if remaining_with_setup_started or not self._previous_was_empty: - async_dispatcher_send_internal( - self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started - ) - self._previous_was_empty = not remaining_with_setup_started - - def _async_schedule_next(self) -> None: - """Schedule the next call.""" - self._handle = self._loop.call_later( - SLOW_STARTUP_CHECK_INTERVAL, self._async_watch - ) - - def async_start(self) -> None: - """Start watching.""" - self._async_schedule_next() - - def async_stop(self) -> None: - """Stop watching.""" - self._async_dispatch({}) - if self._handle: - self._handle.cancel() - self._handle = None - - -async def async_setup_multi_components( - hass: core.HomeAssistant, - domains: set[str], - config: dict[str, Any], -) -> None: - """Set up multiple domains. Log on failure.""" - # Avoid creating tasks for domains that were setup in a previous stage - domains_not_yet_setup = domains - hass.config.components - # Create setup tasks for base platforms first since everything will have - # to wait to be imported, and the sooner we can get the base platforms - # loaded the sooner we can start loading the rest of the integrations. - futures = { - domain: hass.async_create_task_internal( - async_setup_component(hass, domain, config), - f"setup component {domain}", - eager_start=True, - ) - for domain in sorted( - domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True - ) - } - results = await asyncio.gather(*futures.values(), return_exceptions=True) - for idx, domain in enumerate(futures): - result = results[idx] - if isinstance(result, BaseException): - _LOGGER.error( - "Error setting up integration %s - received exception", - domain, - exc_info=(type(result), result, result.__traceback__), - ) - - async def _async_resolve_domains_to_setup( hass: core.HomeAssistant, config: dict[str, Any] ) -> tuple[set[str], dict[str, loader.Integration]]: @@ -1038,7 +935,7 @@ async def _async_set_up_integrations( for dep in integration.all_dependencies ) async_set_domains_to_be_loaded(hass, to_be_loaded) - await async_setup_multi_components(hass, domain_group, config) + await _async_setup_multi_components(hass, domain_group, config) # Enables after dependencies when setting up stage 1 domains async_set_domains_to_be_loaded(hass, stage_1_domains) @@ -1050,7 +947,7 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components(hass, stage_1_domains, config) + await _async_setup_multi_components(hass, stage_1_domains, config) except TimeoutError: _LOGGER.warning( "Setup timed out for stage 1 waiting on %s - moving forward", @@ -1066,7 +963,7 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components(hass, stage_2_domains, config) + await _async_setup_multi_components(hass, stage_2_domains, config) except TimeoutError: _LOGGER.warning( "Setup timed out for stage 2 waiting on %s - moving forward", @@ -1092,3 +989,106 @@ async def _async_set_up_integrations( "Integration setup times: %s", dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)), ) + + +class _WatchPendingSetups: + """Periodic log and dispatch of setups that are pending.""" + + def __init__( + self, + hass: core.HomeAssistant, + setup_started: dict[tuple[str, str | None], float], + ) -> None: + """Initialize the WatchPendingSetups class.""" + self._hass = hass + self._setup_started = setup_started + self._duration_count = 0 + self._handle: asyncio.TimerHandle | None = None + self._previous_was_empty = True + self._loop = hass.loop + + def _async_watch(self) -> None: + """Periodic log of setups that are pending.""" + now = monotonic() + self._duration_count += SLOW_STARTUP_CHECK_INTERVAL + + remaining_with_setup_started: defaultdict[str, float] = defaultdict(float) + for integration_group, start_time in self._setup_started.items(): + domain, _ = integration_group + remaining_with_setup_started[domain] += now - start_time + + if remaining_with_setup_started: + _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + elif waiting_tasks := self._hass._active_tasks: # noqa: SLF001 + _LOGGER.debug("Waiting on tasks: %s", waiting_tasks) + self._async_dispatch(remaining_with_setup_started) + if ( + self._setup_started + and self._duration_count % LOG_SLOW_STARTUP_INTERVAL == 0 + ): + # We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done + # once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up + _LOGGER.warning( + "Waiting on integrations to complete setup: %s", + self._setup_started, + ) + + _LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones) + self._async_schedule_next() + + def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None: + """Dispatch the signal.""" + if remaining_with_setup_started or not self._previous_was_empty: + async_dispatcher_send_internal( + self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started + ) + self._previous_was_empty = not remaining_with_setup_started + + def _async_schedule_next(self) -> None: + """Schedule the next call.""" + self._handle = self._loop.call_later( + SLOW_STARTUP_CHECK_INTERVAL, self._async_watch + ) + + def async_start(self) -> None: + """Start watching.""" + self._async_schedule_next() + + def async_stop(self) -> None: + """Stop watching.""" + self._async_dispatch({}) + if self._handle: + self._handle.cancel() + self._handle = None + + +async def _async_setup_multi_components( + hass: core.HomeAssistant, + domains: set[str], + config: dict[str, Any], +) -> None: + """Set up multiple domains. Log on failure.""" + # Avoid creating tasks for domains that were setup in a previous stage + domains_not_yet_setup = domains - hass.config.components + # Create setup tasks for base platforms first since everything will have + # to wait to be imported, and the sooner we can get the base platforms + # loaded the sooner we can start loading the rest of the integrations. + futures = { + domain: hass.async_create_task_internal( + async_setup_component(hass, domain, config), + f"setup component {domain}", + eager_start=True, + ) + for domain in sorted( + domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True + ) + } + results = await asyncio.gather(*futures.values(), return_exceptions=True) + for idx, domain in enumerate(futures): + result = results[idx] + if isinstance(result, BaseException): + _LOGGER.error( + "Error setting up integration %s - received exception", + domain, + exc_info=(type(result), result, result.__traceback__), + ) diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 78a49b8b877..d0250a382e9 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -16,7 +16,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key." + "requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key." } }, "entity": { diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index ad3ee5fca4d..18ad1c8c402 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -6,21 +6,18 @@ from datetime import timedelta import logging from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_USE_NEAREST, DOMAIN, MIN_UPDATE_INTERVAL -from .coordinator import AirlyDataUpdateCoordinator +from .coordinator import AirlyConfigEntry, AirlyDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -type AirlyConfigEntry = ConfigEntry[AirlyDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> bool: """Set up Airly as config entry.""" @@ -60,7 +57,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> boo update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) coordinator = AirlyDataUpdateCoordinator( - hass, websession, api_key, latitude, longitude, update_interval, use_nearest + hass, + entry, + websession, + api_key, + latitude, + longitude, + update_interval, + use_nearest, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/airly/coordinator.py b/homeassistant/components/airly/coordinator.py index fa826ba6efc..b255c5f078f 100644 --- a/homeassistant/components/airly/coordinator.py +++ b/homeassistant/components/airly/coordinator.py @@ -10,6 +10,7 @@ from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -27,6 +28,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type AirlyConfigEntry = ConfigEntry[AirlyDataUpdateCoordinator] + def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta: """Return data update interval. @@ -58,9 +61,12 @@ def set_update_interval(instances_count: int, requests_remaining: int) -> timede class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]): """Define an object to hold Airly data.""" + config_entry: AirlyConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: AirlyConfigEntry, session: ClientSession, api_key: str, latitude: float, @@ -76,7 +82,13 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i self.airly = Airly(api_key, session, language=language) self.use_nearest = use_nearest - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=update_interval, + ) async def _async_update_data(self) -> dict[str, str | float | int]: """Update data via library.""" diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index 8bf75baf1d1..6e9e55a4311 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -13,7 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import AirlyConfigEntry +from .coordinator import AirlyConfigEntry TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID} diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 2126b838269..fbf73ed753e 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -24,7 +24,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AirlyConfigEntry, AirlyDataUpdateCoordinator from .const import ( ATTR_ADVICE, ATTR_API_ADVICE, @@ -52,6 +51,7 @@ from .const import ( SUFFIX_PERCENT, URL, ) +from .coordinator import AirlyConfigEntry, AirlyDataUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py index 688b6d06189..629cb255122 100644 --- a/homeassistant/components/airly/system_health.py +++ b/homeassistant/components/airly/system_health.py @@ -9,8 +9,8 @@ from airly import Airly from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from . import AirlyConfigEntry from .const import DOMAIN +from .coordinator import AirlyConfigEntry @callback diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 2047a9d41bc..6fb7e90502f 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -15,13 +15,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import AirNowDataUpdateCoordinator +from .coordinator import AirNowConfigEntry, AirNowDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -type AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool: """Set up AirNow from a config entry.""" @@ -38,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bo # Setup the Coordinator session = async_get_clientsession(hass) coordinator = AirNowDataUpdateCoordinator( - hass, session, api_key, latitude, longitude, distance, update_interval + hass, entry, session, api_key, latitude, longitude, distance, update_interval ) # Sync with Coordinator diff --git a/homeassistant/components/airnow/coordinator.py b/homeassistant/components/airnow/coordinator.py index 9434d368dbe..ee5bf4a1dd7 100644 --- a/homeassistant/components/airnow/coordinator.py +++ b/homeassistant/components/airnow/coordinator.py @@ -10,6 +10,7 @@ from pyairnow import WebServiceAPI from pyairnow.conv import aqi_to_concentration from pyairnow.errors import AirNowError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -34,13 +35,18 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator] + class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """The AirNow update coordinator.""" + config_entry: AirNowConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: AirNowConfigEntry, session: ClientSession, api_key: str, latitude: float, @@ -55,7 +61,13 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.airnow = WebServiceAPI(api_key, session=session) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=update_interval, + ) async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" diff --git a/homeassistant/components/airnow/diagnostics.py b/homeassistant/components/airnow/diagnostics.py index 76cc35fb13c..bd6dab9dc47 100644 --- a/homeassistant/components/airnow/diagnostics.py +++ b/homeassistant/components/airnow/diagnostics.py @@ -13,7 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import AirNowConfigEntry +from .coordinator import AirNowConfigEntry ATTR_LATITUDE_CAP = "Latitude" ATTR_LONGITUDE_CAP = "Longitude" diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 1abf93514a5..c8b1c985c8b 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -25,7 +25,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AirNowConfigEntry, AirNowDataUpdateCoordinator from .const import ( ATTR_API_AQI, ATTR_API_AQI_DESCRIPTION, @@ -43,6 +42,7 @@ from .const import ( DOMAIN, US_TZ_OFFSETS, ) +from .coordinator import AirNowConfigEntry, AirNowDataUpdateCoordinator ATTRIBUTION = "Data provided by AirNow" diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index aa168dce858..a56f2cc2445 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -15,7 +15,6 @@ from aioairzone.const import ( ) from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -25,7 +24,7 @@ from homeassistant.helpers import ( ) from .const import DOMAIN, MANUFACTURER -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -38,8 +37,6 @@ PLATFORMS: list[Platform] = [ _LOGGER = logging.getLogger(__name__) -type AirzoneConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] - async def _async_migrate_unique_ids( hass: HomeAssistant, @@ -90,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b ) airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) - coordinator = AirzoneUpdateCoordinator(hass, airzone) + coordinator = AirzoneUpdateCoordinator(hass, entry, airzone) await coordinator.async_config_entry_first_refresh() await _async_migrate_unique_ids(hass, entry, coordinator) diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index eec78156fe0..48f6ce8fd94 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -25,8 +25,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 4ed54286cff..23355a070ab 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -50,9 +50,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry from .const import API_TEMPERATURE_STEP, TEMP_UNIT_LIB_TO_HASS -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneZoneEntity BASE_FAN_SPEEDS: Final[dict[int, str]] = { diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index 8ec2cbe07ca..4b4519beed8 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -10,6 +10,7 @@ from typing import Any from aioairzone.exceptions import AirzoneError from aioairzone.localapi import AirzoneLocalApi +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,17 +20,27 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) +type AirzoneConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] + class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Airzone device.""" - def __init__(self, hass: HomeAssistant, airzone: AirzoneLocalApi) -> None: + config_entry: AirzoneConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: AirzoneConfigEntry, + airzone: AirzoneLocalApi, + ) -> None: """Initialize.""" self.airzone = airzone super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/airzone/diagnostics.py b/homeassistant/components/airzone/diagnostics.py index 2945df7b6fb..e745a85ee5e 100644 --- a/homeassistant/components/airzone/diagnostics.py +++ b/homeassistant/components/airzone/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from . import AirzoneConfigEntry +from .coordinator import AirzoneConfigEntry TO_REDACT_API = [ API_MAC, diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 59d58fb62b0..c0d7901981b 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -31,9 +31,8 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AirzoneConfigEntry from .const import DOMAIN, MANUFACTURER -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py index 2bc11bc4228..56a9b06ea21 100644 --- a/homeassistant/components/airzone/select.py +++ b/homeassistant/components/airzone/select.py @@ -27,8 +27,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index ef8ddbb3b65..0b5c5666c89 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -30,9 +30,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry from .const import TEMP_UNIT_LIB_TO_HASS -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import ( AirzoneEntity, AirzoneHotWaterEntity, diff --git a/homeassistant/components/airzone/switch.py b/homeassistant/components/airzone/switch.py index 93136810604..69bf33666a5 100644 --- a/homeassistant/components/airzone/switch.py +++ b/homeassistant/components/airzone/switch.py @@ -16,8 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity diff --git a/homeassistant/components/airzone/water_heater.py b/homeassistant/components/airzone/water_heater.py index 8fd563b33d8..2a1ca72db21 100644 --- a/homeassistant/components/airzone/water_heater.py +++ b/homeassistant/components/airzone/water_heater.py @@ -30,9 +30,8 @@ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneConfigEntry from .const import TEMP_UNIT_LIB_TO_HASS -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneHotWaterEntity OPERATION_LIB_TO_HASS: Final[dict[HotWaterOperation, str]] = { diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index 29d8f166f2a..9eab6f42ad3 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -2,14 +2,11 @@ import amberelectric -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant from .const import CONF_SITE_ID, PLATFORMS -from .coordinator import AmberUpdateCoordinator - -type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] +from .coordinator import AmberConfigEntry, AmberUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool: @@ -19,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> boo api_instance = amberelectric.AmberApi(api_client) site_id = entry.data[CONF_SITE_ID] - coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) + coordinator = AmberUpdateCoordinator(hass, entry, api_instance, site_id) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index a9fa00d0129..66292ea5524 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -12,9 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AmberConfigEntry from .const import ATTRIBUTION -from .coordinator import AmberUpdateCoordinator +from .coordinator import AmberConfigEntry, AmberUpdateCoordinator PRICE_SPIKE_ICONS = { "none": "mdi:power-plug", diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index 57028e07d21..1edf64ba0d6 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -13,11 +13,14 @@ from amberelectric.models.forecast_interval import ForecastInterval from amberelectric.models.price_descriptor import PriceDescriptor from amberelectric.rest import ApiException +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER +type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] + def is_current(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool: """Return true if the supplied interval is a CurrentInterval.""" @@ -70,13 +73,20 @@ def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None: class AmberUpdateCoordinator(DataUpdateCoordinator): """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read.""" + config_entry: AmberConfigEntry + def __init__( - self, hass: HomeAssistant, api: amberelectric.AmberApi, site_id: str + self, + hass: HomeAssistant, + config_entry: AmberConfigEntry, + api: amberelectric.AmberApi, + site_id: str, ) -> None: """Initialise the data service.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name="amberelectric", update_interval=timedelta(minutes=1), ) diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index cdf40e5804d..49d6e5f4eac 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -22,9 +22,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AmberConfigEntry from .const import ATTRIBUTION -from .coordinator import AmberUpdateCoordinator, normalize_descriptor +from .coordinator import AmberConfigEntry, AmberUpdateCoordinator, normalize_descriptor UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" diff --git a/homeassistant/components/ambient_network/__init__.py b/homeassistant/components/ambient_network/__init__.py index e9443a676b5..5c39982eb91 100644 --- a/homeassistant/components/ambient_network/__init__.py +++ b/homeassistant/components/ambient_network/__init__.py @@ -4,13 +4,10 @@ from __future__ import annotations from aioambient.open_api import OpenAPI -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .coordinator import AmbientNetworkDataUpdateCoordinator - -type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator] +from .coordinator import AmbientNetworkConfigEntry, AmbientNetworkDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -21,7 +18,7 @@ async def async_setup_entry( """Set up the Ambient Weather Network from a config entry.""" api = OpenAPI() - coordinator = AmbientNetworkDataUpdateCoordinator(hass, api) + coordinator = AmbientNetworkDataUpdateCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ambient_network/coordinator.py b/homeassistant/components/ambient_network/coordinator.py index 2f51c3bc0cb..5fb1939f6b4 100644 --- a/homeassistant/components/ambient_network/coordinator.py +++ b/homeassistant/components/ambient_network/coordinator.py @@ -19,17 +19,27 @@ from .helper import get_station_name SCAN_INTERVAL = timedelta(minutes=5) +type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator] + class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """The Ambient Network Data Update Coordinator.""" - config_entry: ConfigEntry + config_entry: AmbientNetworkConfigEntry station_name: str last_measured: datetime | None = None - def __init__(self, hass: HomeAssistant, api: OpenAPI) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: AmbientNetworkConfigEntry, api: OpenAPI + ) -> None: """Initialize the coordinator.""" - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + super().__init__( + hass, + LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) self.api = api async def _async_update_data(self) -> dict[str, Any]: diff --git a/homeassistant/components/ambient_network/sensor.py b/homeassistant/components/ambient_network/sensor.py index 336745f88ff..9d262e5a987 100644 --- a/homeassistant/components/ambient_network/sensor.py +++ b/homeassistant/components/ambient_network/sensor.py @@ -28,8 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from . import AmbientNetworkConfigEntry -from .coordinator import AmbientNetworkDataUpdateCoordinator +from .coordinator import AmbientNetworkConfigEntry, AmbientNetworkDataUpdateCoordinator from .entity import AmbientNetworkEntity TYPE_AQI_PM25 = "aqi_pm25" diff --git a/homeassistant/components/analytics_insights/__init__.py b/homeassistant/components/analytics_insights/__init__.py index 69ad98db9df..ee7f6611c65 100644 --- a/homeassistant/components/analytics_insights/__init__.py +++ b/homeassistant/components/analytics_insights/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry( continue names[integration] = integrations[integration].title - coordinator = HomeassistantAnalyticsDataUpdateCoordinator(hass, client) + coordinator = HomeassistantAnalyticsDataUpdateCoordinator(hass, entry, client) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/analytics_insights/coordinator.py b/homeassistant/components/analytics_insights/coordinator.py index 701f1a8dbd4..fefd43ed8df 100644 --- a/homeassistant/components/analytics_insights/coordinator.py +++ b/homeassistant/components/analytics_insights/coordinator.py @@ -46,12 +46,16 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic config_entry: AnalyticsInsightsConfigEntry def __init__( - self, hass: HomeAssistant, client: HomeassistantAnalyticsClient + self, + hass: HomeAssistant, + config_entry: AnalyticsInsightsConfigEntry, + client: HomeassistantAnalyticsClient, ) -> None: """Initialize the Homeassistant Analytics data coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(hours=12), ) diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py index fd6e1fcc4b9..c72d6ae1177 100644 --- a/homeassistant/components/android_ip_webcam/coordinator.py +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -35,6 +35,7 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): super().__init__( self.hass, _LOGGER, + config_entry=config_entry, name=f"{DOMAIN} {config_entry.data[CONF_HOST]}", update_interval=timedelta(seconds=10), ) diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index 6a55e9971ac..28a372da4ea 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -35,16 +35,12 @@ async def async_setup_entry( @callback def is_available_updated(is_available: bool) -> None: - if is_available: - _LOGGER.info( - "Reconnected to %s at %s", entry.data[CONF_NAME], entry.data[CONF_HOST] - ) - else: - _LOGGER.warning( - "Disconnected from %s at %s", - entry.data[CONF_NAME], - entry.data[CONF_HOST], - ) + _LOGGER.info( + "%s %s at %s", + "Reconnected to" if is_available else "Disconnected from", + entry.data[CONF_NAME], + entry.data[CONF_HOST], + ) api.add_is_available_updated_callback(is_available_updated) diff --git a/homeassistant/components/anova/__init__.py b/homeassistant/components/anova/__init__.py index 4ae4750b9a9..9307cfc4bdd 100644 --- a/homeassistant/components/anova/__init__.py +++ b/homeassistant/components/anova/__init__.py @@ -18,8 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from .coordinator import AnovaCoordinator -from .models import AnovaConfigEntry, AnovaData +from .coordinator import AnovaConfigEntry, AnovaCoordinator, AnovaData PLATFORMS = [Platform.SENSOR] @@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> boo # websocket client assert api.websocket_handler is not None devices = list(api.websocket_handler.devices.values()) - coordinators = [AnovaCoordinator(hass, device) for device in devices] + coordinators = [AnovaCoordinator(hass, entry, device) for device in devices] entry.runtime_data = AnovaData(api_jwt=api.jwt, coordinators=coordinators, api=api) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py index 93c6fdbf1c5..811c32c97b5 100644 --- a/homeassistant/components/anova/coordinator.py +++ b/homeassistant/components/anova/coordinator.py @@ -1,8 +1,9 @@ """Support for Anova Coordinators.""" +from dataclasses import dataclass import logging -from anova_wifi import APCUpdate, APCWifiDevice +from anova_wifi import AnovaApi, APCUpdate, APCWifiDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -14,15 +15,33 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +@dataclass +class AnovaData: + """Data for the Anova integration.""" + + api_jwt: str + coordinators: list["AnovaCoordinator"] + api: AnovaApi + + +type AnovaConfigEntry = ConfigEntry[AnovaData] + + class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]): """Anova custom coordinator.""" - config_entry: ConfigEntry + config_entry: AnovaConfigEntry - def __init__(self, hass: HomeAssistant, anova_device: APCWifiDevice) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: AnovaConfigEntry, + anova_device: APCWifiDevice, + ) -> None: """Set up Anova Coordinator.""" super().__init__( hass, + config_entry=config_entry, name="Anova Precision Cooker", logger=_LOGGER, ) diff --git a/homeassistant/components/anova/models.py b/homeassistant/components/anova/models.py deleted file mode 100644 index eef8180cf88..00000000000 --- a/homeassistant/components/anova/models.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Dataclass models for the Anova integration.""" - -from dataclasses import dataclass - -from anova_wifi import AnovaApi - -from homeassistant.config_entries import ConfigEntry - -from .coordinator import AnovaCoordinator - -type AnovaConfigEntry = ConfigEntry[AnovaData] - - -@dataclass -class AnovaData: - """Data for the Anova integration.""" - - api_jwt: str - coordinators: list[AnovaCoordinator] - api: AnovaApi diff --git a/homeassistant/components/anova/sensor.py b/homeassistant/components/anova/sensor.py index aa572a0ee9b..7365e4597ba 100644 --- a/homeassistant/components/anova/sensor.py +++ b/homeassistant/components/anova/sensor.py @@ -18,9 +18,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .coordinator import AnovaCoordinator +from .coordinator import AnovaConfigEntry, AnovaCoordinator from .entity import AnovaDescriptionEntity -from .models import AnovaConfigEntry @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 44edc5c151f..e444f1cd735 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -4,13 +4,10 @@ from __future__ import annotations from typing import Final -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from .coordinator import APCUPSdCoordinator - -type APCUPSdConfigEntry = ConfigEntry[APCUPSdCoordinator] +from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR) @@ -20,7 +17,7 @@ async def async_setup_entry( ) -> bool: """Use config values to set up a function enabling status retrieval.""" host, port = config_entry.data[CONF_HOST], config_entry.data[CONF_PORT] - coordinator = APCUPSdCoordinator(hass, host, port) + coordinator = APCUPSdCoordinator(hass, config_entry, host, port) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index cd9e60f7ae4..2a44845618e 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -12,8 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import APCUPSdConfigEntry -from .coordinator import APCUPSdCoordinator +from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/apcupsd/coordinator.py b/homeassistant/components/apcupsd/coordinator.py index 1ae12d8c4b0..e2c1af50cee 100644 --- a/homeassistant/components/apcupsd/coordinator.py +++ b/homeassistant/components/apcupsd/coordinator.py @@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL: Final = timedelta(seconds=60) REQUEST_REFRESH_COOLDOWN: Final = 5 +type APCUPSdConfigEntry = ConfigEntry[APCUPSdCoordinator] + class APCUPSdData(dict[str, str]): """Store data about an APCUPSd and provide a few helper methods for easier accesses.""" @@ -57,13 +59,20 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]): updates from the server. """ - config_entry: ConfigEntry + config_entry: APCUPSdConfigEntry - def __init__(self, hass: HomeAssistant, host: str, port: int) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: APCUPSdConfigEntry, + host: str, + port: int, + ) -> None: """Initialize the data object.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=UPDATE_INTERVAL, request_refresh_debouncer=Debouncer( diff --git a/homeassistant/components/apcupsd/diagnostics.py b/homeassistant/components/apcupsd/diagnostics.py index fa0908f3144..a4bbf2191d2 100644 --- a/homeassistant/components/apcupsd/diagnostics.py +++ b/homeassistant/components/apcupsd/diagnostics.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.core import HomeAssistant -from . import APCUPSdConfigEntry +from .coordinator import APCUPSdConfigEntry TO_REDACT = {"SERIALNO", "HOSTNAME"} diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 9e0abcb1dd9..b3c396daf5e 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -24,9 +24,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import APCUPSdConfigEntry from .const import LAST_S_TEST -from .coordinator import APCUPSdCoordinator +from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/aquacell/__init__.py b/homeassistant/components/aquacell/__init__.py index e44c0f00fa8..60787840fcb 100644 --- a/homeassistant/components/aquacell/__init__.py +++ b/homeassistant/components/aquacell/__init__.py @@ -5,18 +5,15 @@ from __future__ import annotations from aioaquacell import AquacellApi from aioaquacell.const import Brand -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_BRAND -from .coordinator import AquacellCoordinator +from .coordinator import AquacellConfigEntry, AquacellCoordinator PLATFORMS = [Platform.SENSOR] -type AquacellConfigEntry = ConfigEntry[AquacellCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> bool: """Set up Aquacell from a config entry.""" @@ -26,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> aquacell_api = AquacellApi(session, brand) - coordinator = AquacellCoordinator(hass, aquacell_api) + coordinator = AquacellCoordinator(hass, entry, aquacell_api) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -36,6 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aquacell/coordinator.py b/homeassistant/components/aquacell/coordinator.py index ee4afb451b9..135ab0ecb50 100644 --- a/homeassistant/components/aquacell/coordinator.py +++ b/homeassistant/components/aquacell/coordinator.py @@ -26,17 +26,25 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type AquacellConfigEntry = ConfigEntry[AquacellCoordinator] + class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]): """My aquacell coordinator.""" - config_entry: ConfigEntry + config_entry: AquacellConfigEntry - def __init__(self, hass: HomeAssistant, aquacell_api: AquacellApi) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: AquacellConfigEntry, + aquacell_api: AquacellApi, + ) -> None: """Initialize coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Aquacell Coordinator", update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/aquacell/sensor.py b/homeassistant/components/aquacell/sensor.py index 702d75a0215..a76d26244ad 100644 --- a/homeassistant/components/aquacell/sensor.py +++ b/homeassistant/components/aquacell/sensor.py @@ -18,8 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import AquacellConfigEntry -from .coordinator import AquacellCoordinator +from .coordinator import AquacellConfigEntry, AquacellCoordinator from .entity import AquacellEntity PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/arve/__init__.py b/homeassistant/components/arve/__init__.py index a1b4aa7042e..c5900967bde 100644 --- a/homeassistant/components/arve/__init__.py +++ b/homeassistant/components/arve/__init__.py @@ -13,7 +13,7 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool: """Set up Arve from a config entry.""" - coordinator = ArveCoordinator(hass) + coordinator = ArveCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/arve/coordinator.py b/homeassistant/components/arve/coordinator.py index f02220e28e2..4b08efd639e 100644 --- a/homeassistant/components/arve/coordinator.py +++ b/homeassistant/components/arve/coordinator.py @@ -30,11 +30,12 @@ class ArveCoordinator(DataUpdateCoordinator[ArveSensProData]): config_entry: ArveConfigEntry devices: ArveDevices - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ArveConfigEntry) -> None: """Initialize Arve coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 94e2b04d7ae..ef26e1a5a6d 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1093,16 +1093,18 @@ class PipelineRun: agent_id = conversation.HOME_ASSISTANT_AGENT processed_locally = True - # It was already handled, create response and add to chat history - if intent_response is not None: - with ( - chat_session.async_get_chat_session( - self.hass, user_input.conversation_id - ) as session, - conversation.async_get_chat_log( - self.hass, session, user_input - ) as chat_log, - ): + with ( + chat_session.async_get_chat_session( + self.hass, user_input.conversation_id + ) as session, + conversation.async_get_chat_log( + self.hass, + session, + user_input, + ) as chat_log, + ): + # It was already handled, create response and add to chat history + if intent_response is not None: speech: str = intent_response.speech.get("plain", {}).get( "speech", "" ) @@ -1117,21 +1119,21 @@ class PipelineRun: conversation_id=session.conversation_id, ) - else: - # Fall back to pipeline conversation agent - conversation_result = await conversation.async_converse( - hass=self.hass, - text=user_input.text, - conversation_id=user_input.conversation_id, - device_id=user_input.device_id, - context=user_input.context, - language=user_input.language, - agent_id=user_input.agent_id, - extra_system_prompt=user_input.extra_system_prompt, - ) - speech = conversation_result.response.speech.get("plain", {}).get( - "speech", "" - ) + else: + # Fall back to pipeline conversation agent + conversation_result = await conversation.async_converse( + hass=self.hass, + text=user_input.text, + conversation_id=user_input.conversation_id, + device_id=user_input.device_id, + context=user_input.context, + language=user_input.language, + agent_id=user_input.agent_id, + extra_system_prompt=user_input.extra_system_prompt, + ) + speech = conversation_result.response.speech.get("plain", {}).get( + "speech", "" + ) except Exception as src_error: _LOGGER.exception("Unexpected error during intent recognition") diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 749d40aeb5c..91166d5c8f5 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AuroraAbbConfigEntry) -> comport = entry.data[CONF_PORT] address = entry.data[CONF_ADDRESS] - coordinator = AuroraAbbDataUpdateCoordinator(hass, comport, address) + coordinator = AuroraAbbDataUpdateCoordinator(hass, entry, comport, address) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/aurora_abb_powerone/coordinator.py b/homeassistant/components/aurora_abb_powerone/coordinator.py index c3d05da95f3..d38f0716b44 100644 --- a/homeassistant/components/aurora_abb_powerone/coordinator.py +++ b/homeassistant/components/aurora_abb_powerone/coordinator.py @@ -21,12 +21,26 @@ type AuroraAbbConfigEntry = ConfigEntry[AuroraAbbDataUpdateCoordinator] class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]): """Class to manage fetching AuroraAbbPowerone data.""" - def __init__(self, hass: HomeAssistant, comport: str, address: int) -> None: + config_entry: AuroraAbbConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: AuroraAbbConfigEntry, + comport: str, + address: int, + ) -> None: """Initialize the data update coordinator.""" self.available_prev = False self.available = False self.client = AuroraSerialClient(address, comport, parity="N", timeout=1) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) def _update_data(self) -> dict[str, float]: """Fetch new state data for the sensors. diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 52b48b1d0d6..673df594e89 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry( # Initiate a Data Update Coordinator for each service for service in services: service["coordinator"] = AussieBroadbandDataUpdateCoordinator( - hass, client, service["service_id"] + hass, entry, client, service["service_id"] ) await service["coordinator"].async_config_entry_first_refresh() diff --git a/homeassistant/components/aussie_broadband/coordinator.py b/homeassistant/components/aussie_broadband/coordinator.py index 844442985c0..20987c5f30f 100644 --- a/homeassistant/components/aussie_broadband/coordinator.py +++ b/homeassistant/components/aussie_broadband/coordinator.py @@ -34,11 +34,20 @@ type AussieBroadbandConfigEntry = ConfigEntry[list[AussieBroadbandServiceData]] class AussieBroadbandDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Aussie Broadand data update coordinator.""" - def __init__(self, hass: HomeAssistant, client: AussieBB, service_id: str) -> None: + config_entry: AussieBroadbandConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: AussieBroadbandConfigEntry, + client: AussieBB, + service_id: str, + ) -> None: """Initialize Atag coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"Aussie Broadband {service_id}", update_interval=timedelta(minutes=DEFAULT_UPDATE_INTERVAL), ) diff --git a/homeassistant/components/autarco/__init__.py b/homeassistant/components/autarco/__init__.py index f42bfdf4a0e..a524535c122 100644 --- a/homeassistant/components/autarco/__init__.py +++ b/homeassistant/components/autarco/__init__.py @@ -6,18 +6,15 @@ import asyncio from autarco import Autarco, AutarcoConnectionError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import AutarcoDataUpdateCoordinator +from .coordinator import AutarcoConfigEntry, AutarcoDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]] - async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool: """Set up Autarco from a config entry.""" @@ -34,7 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> b raise ConfigEntryNotReady from err coordinators: list[AutarcoDataUpdateCoordinator] = [ - AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites + AutarcoDataUpdateCoordinator(hass, entry, client, site) + for site in account_sites ] await asyncio.gather( diff --git a/homeassistant/components/autarco/coordinator.py b/homeassistant/components/autarco/coordinator.py index dd8786bca25..deb40155443 100644 --- a/homeassistant/components/autarco/coordinator.py +++ b/homeassistant/components/autarco/coordinator.py @@ -22,6 +22,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER, SCAN_INTERVAL +type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]] + class AutarcoData(NamedTuple): """Class for defining data in dict.""" @@ -35,11 +37,12 @@ class AutarcoData(NamedTuple): class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]): """Class to manage fetching Autarco data from the API.""" - config_entry: ConfigEntry + config_entry: AutarcoConfigEntry def __init__( self, hass: HomeAssistant, + config_entry: AutarcoConfigEntry, client: Autarco, account_site: AccountSite, ) -> None: @@ -47,6 +50,7 @@ class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]): super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/autarco/diagnostics.py b/homeassistant/components/autarco/diagnostics.py index c865a38ffd8..a2dd0c5a361 100644 --- a/homeassistant/components/autarco/diagnostics.py +++ b/homeassistant/components/autarco/diagnostics.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.core import HomeAssistant -from . import AutarcoConfigEntry, AutarcoDataUpdateCoordinator +from .coordinator import AutarcoConfigEntry, AutarcoDataUpdateCoordinator async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/autarco/sensor.py b/homeassistant/components/autarco/sensor.py index c870197a504..b7c4312815b 100644 --- a/homeassistant/components/autarco/sensor.py +++ b/homeassistant/components/autarco/sensor.py @@ -20,9 +20,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AutarcoConfigEntry from .const import DOMAIN -from .coordinator import AutarcoDataUpdateCoordinator +from .coordinator import AutarcoConfigEntry, AutarcoDataUpdateCoordinator @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 9890d47fbb5..0522e9778df 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -9,9 +9,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import CONF_PAT, CONF_PROJECT -from .coordinator import AzureDevOpsDataUpdateCoordinator - -type AzureDevOpsConfigEntry = ConfigEntry[AzureDevOpsDataUpdateCoordinator] +from .coordinator import AzureDevOpsConfigEntry, AzureDevOpsDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -22,11 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AzureDevOpsConfigEntry) """Set up Azure DevOps from a config entry.""" # Create the data update coordinator - coordinator = AzureDevOpsDataUpdateCoordinator( - hass, - _LOGGER, - entry=entry, - ) + coordinator = AzureDevOpsDataUpdateCoordinator(hass, entry, _LOGGER) # Store the coordinator in runtime data entry.runtime_data = coordinator diff --git a/homeassistant/components/azure_devops/coordinator.py b/homeassistant/components/azure_devops/coordinator.py index 21fb76560c3..efbecfbcd19 100644 --- a/homeassistant/components/azure_devops/coordinator.py +++ b/homeassistant/components/azure_devops/coordinator.py @@ -28,6 +28,8 @@ from .data import AzureDevOpsData BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" IGNORED_CATEGORIES: Final[list[Category]] = [Category.COMPLETED, Category.REMOVED] +type AzureDevOpsConfigEntry = ConfigEntry[AzureDevOpsDataUpdateCoordinator] + def ado_exception_none_handler(func: Callable) -> Callable: """Handle exceptions or None to always return a value or raise.""" @@ -50,28 +52,29 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): """Class to manage and fetch Azure DevOps data.""" client: DevOpsClient + config_entry: AzureDevOpsConfigEntry organization: str project: Project def __init__( self, hass: HomeAssistant, + config_entry: AzureDevOpsConfigEntry, logger: logging.Logger, - *, - entry: ConfigEntry, ) -> None: """Initialize global Azure DevOps data updater.""" - self.title = entry.title + self.title = config_entry.title super().__init__( hass=hass, logger=logger, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=300), ) self.client = DevOpsClient(session=async_get_clientsession(hass)) - self.organization = entry.data[CONF_ORG] + self.organization = config_entry.data[CONF_ORG] @ado_exception_none_handler async def authorize( diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index fd47115214a..1d590032a85 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -21,8 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util -from . import AzureDevOpsConfigEntry -from .coordinator import AzureDevOpsDataUpdateCoordinator +from .coordinator import AzureDevOpsConfigEntry, AzureDevOpsDataUpdateCoordinator from .entity import AzureDevOpsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index f6516434cd2..d328849e6fe 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> boo auth_data = deepcopy(dict(entry.data)) blink.auth = Auth(auth_data, no_prompt=True, session=session) blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - coordinator = BlinkUpdateCoordinator(hass, blink) + coordinator = BlinkUpdateCoordinator(hass, entry, blink) try: await blink.start() diff --git a/homeassistant/components/blink/coordinator.py b/homeassistant/components/blink/coordinator.py index 7278dabe083..b9a0db101b4 100644 --- a/homeassistant/components/blink/coordinator.py +++ b/homeassistant/components/blink/coordinator.py @@ -23,12 +23,17 @@ type BlinkConfigEntry = ConfigEntry[BlinkUpdateCoordinator] class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """BlinkUpdateCoordinator - In charge of downloading the data for a site.""" - def __init__(self, hass: HomeAssistant, api: Blink) -> None: + config_entry: BlinkConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: BlinkConfigEntry, api: Blink + ) -> None: """Initialize the data service.""" self.api = api super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=SCAN_INTERVAL), ) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 6593afb75d1..52fcf82b38b 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -7,14 +7,11 @@ from typing import Final from aiohttp import CookieJar from pybravia import BraviaClient -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .coordinator import BraviaTVCoordinator - -BraviaTVConfigEntry = ConfigEntry[BraviaTVCoordinator] +from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [ Platform.BUTTON, @@ -36,8 +33,8 @@ async def async_setup_entry( client = BraviaClient(host, mac, session=session) coordinator = BraviaTVCoordinator( hass=hass, + config_entry=config_entry, client=client, - config=config_entry.data, ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py index 358255bd85b..626e5a225b7 100644 --- a/homeassistant/components/braviatv/button.py +++ b/homeassistant/components/braviatv/button.py @@ -14,8 +14,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BraviaTVConfigEntry -from .coordinator import BraviaTVCoordinator +from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator from .entity import BraviaTVEntity diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index e08e88073f3..1cc306bd5cf 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -6,7 +6,6 @@ from collections.abc import Awaitable, Callable, Coroutine, Iterable from datetime import datetime, timedelta from functools import wraps import logging -from types import MappingProxyType from typing import Any, Concatenate, Final from pybravia import ( @@ -20,6 +19,7 @@ from pybravia import ( ) from homeassistant.components.media_player import MediaType +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -39,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=10) +type BraviaTVConfigEntry = ConfigEntry["BraviaTVCoordinator"] + def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P]( func: Callable[Concatenate[_BraviaTVCoordinatorT, _P], Awaitable[None]], @@ -64,19 +66,21 @@ def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P]( class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Representation of a Bravia TV Coordinator.""" + config_entry: BraviaTVConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: BraviaTVConfigEntry, client: BraviaClient, - config: MappingProxyType[str, Any], ) -> None: """Initialize Bravia TV Client.""" self.client = client - self.pin = config[CONF_PIN] - self.use_psk = config.get(CONF_USE_PSK, False) - self.client_id = config.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) - self.nickname = config.get(CONF_NICKNAME, NICKNAME_PREFIX) + self.pin = config_entry.data[CONF_PIN] + self.use_psk = config_entry.data.get(CONF_USE_PSK, False) + self.client_id = config_entry.data.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) + self.nickname = config_entry.data.get(CONF_NICKNAME, NICKNAME_PREFIX) self.source: str | None = None self.source_list: list[str] = [] self.source_map: dict[str, dict] = {} @@ -98,6 +102,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, request_refresh_debouncer=Debouncer( diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py index 0969674d5c9..b858fd41c09 100644 --- a/homeassistant/components/braviatv/diagnostics.py +++ b/homeassistant/components/braviatv/diagnostics.py @@ -6,7 +6,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant -from . import BraviaTVConfigEntry +from .coordinator import BraviaTVConfigEntry TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"} diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index 75540b316a7..b4e370f20d2 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -3,8 +3,8 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BraviaTVCoordinator from .const import ATTR_MANUFACTURER, DOMAIN +from .coordinator import BraviaTVCoordinator class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]): diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 4de167a6def..ca48c6ee639 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -18,8 +18,8 @@ from homeassistant.components.media_player import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BraviaTVConfigEntry from .const import SourceType +from .coordinator import BraviaTVConfigEntry from .entity import BraviaTVEntity diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 9344d6ec455..9f4a573827b 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -9,7 +9,7 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BraviaTVConfigEntry +from .coordinator import BraviaTVConfigEntry from .entity import BraviaTVEntity diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index f8c261db3fd..1dbe0adbf6c 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -25,7 +25,7 @@ }, "data_description": { "email": "[%key:component::bring::config::step::user::data_description::email%]", - "password": "[%key:component::bring::config::step::user::data_description::email%]" + "password": "[%key:component::bring::config::step::user::data_description::password%]" } }, "reconfigure": { @@ -37,7 +37,7 @@ }, "data_description": { "email": "[%key:component::bring::config::step::user::data_description::email%]", - "password": "[%key:component::bring::config::step::user::data_description::email%]" + "password": "[%key:component::bring::config::step::user::data_description::password%]" } } }, diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index e828d35f9c7..464e6629224 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -5,17 +5,14 @@ from __future__ import annotations from brother import Brother, SnmpError from homeassistant.components.snmp import async_get_snmp_engine -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .coordinator import BrotherDataUpdateCoordinator +from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -type BrotherConfigEntry = ConfigEntry[BrotherDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool: """Set up Brother from a config entry.""" @@ -30,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b except (ConnectionError, SnmpError, TimeoutError) as error: raise ConfigEntryNotReady from error - coordinator = BrotherDataUpdateCoordinator(hass, brother) + coordinator = BrotherDataUpdateCoordinator(hass, entry, brother) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/brother/coordinator.py b/homeassistant/components/brother/coordinator.py index 69463d107e4..4f518ba8a25 100644 --- a/homeassistant/components/brother/coordinator.py +++ b/homeassistant/components/brother/coordinator.py @@ -5,6 +5,7 @@ import logging from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -12,17 +13,24 @@ from .const import DOMAIN, UPDATE_INTERVAL _LOGGER = logging.getLogger(__name__) +type BrotherConfigEntry = ConfigEntry[BrotherDataUpdateCoordinator] + class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): """Class to manage fetching Brother data from the printer.""" - def __init__(self, hass: HomeAssistant, brother: Brother) -> None: + config_entry: BrotherConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: BrotherConfigEntry, brother: Brother + ) -> None: """Initialize.""" self.brother = brother super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index d4a6c6c5400..33b2e8297e4 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.core import HomeAssistant -from . import BrotherConfigEntry +from .coordinator import BrotherConfigEntry async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index d49ebdf07ca..087a971f928 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -24,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BrotherConfigEntry, BrotherDataUpdateCoordinator from .const import DOMAIN +from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator ATTR_COUNTER = "counter" ATTR_REMAINING_PAGES = "remaining_pages" diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py index be9030d95b0..5c5e23efa8a 100644 --- a/homeassistant/components/bsblan/coordinator.py +++ b/homeassistant/components/bsblan/coordinator.py @@ -38,6 +38,7 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]): super().__init__( hass, logger=LOGGER, + config_entry=config_entry, name=f"{DOMAIN}_{config_entry.data[CONF_HOST]}", update_interval=self._get_update_interval(), ) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index e84ba387194..612610eff43 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -4,23 +4,20 @@ from __future__ import annotations from aioelectricitymaps import ElectricityMaps -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import CO2SignalCoordinator +from .coordinator import CO2SignalConfigEntry, CO2SignalCoordinator PLATFORMS = [Platform.SENSOR] -type CO2SignalConfigEntry = ConfigEntry[CO2SignalCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: CO2SignalConfigEntry) -> bool: """Set up CO2 Signal from a config entry.""" session = async_get_clientsession(hass) coordinator = CO2SignalCoordinator( - hass, ElectricityMaps(token=entry.data[CONF_API_KEY], session=session) + hass, entry, ElectricityMaps(token=entry.data[CONF_API_KEY], session=session) ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py index 475ebd1225d..be2036292e3 100644 --- a/homeassistant/components/co2signal/coordinator.py +++ b/homeassistant/components/co2signal/coordinator.py @@ -22,16 +22,27 @@ from .helpers import fetch_latest_carbon_intensity _LOGGER = logging.getLogger(__name__) +type CO2SignalConfigEntry = ConfigEntry[CO2SignalCoordinator] + class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]): """Data update coordinator.""" - config_entry: ConfigEntry + config_entry: CO2SignalConfigEntry - def __init__(self, hass: HomeAssistant, client: ElectricityMaps) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: CO2SignalConfigEntry, + client: ElectricityMaps, + ) -> None: """Initialize the coordinator.""" super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=15) + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=timedelta(minutes=15), ) self.client = client diff --git a/homeassistant/components/co2signal/diagnostics.py b/homeassistant/components/co2signal/diagnostics.py index a071950440f..840ba759a7b 100644 --- a/homeassistant/components/co2signal/diagnostics.py +++ b/homeassistant/components/co2signal/diagnostics.py @@ -9,7 +9,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from . import CO2SignalConfigEntry +from .coordinator import CO2SignalConfigEntry TO_REDACT = {CONF_API_KEY} diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 1b964edf591..92f88b8ae82 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -18,9 +18,8 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CO2SignalConfigEntry from .const import ATTRIBUTION, DOMAIN -from .coordinator import CO2SignalCoordinator +from .coordinator import CO2SignalConfigEntry, CO2SignalCoordinator @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index f5d9ad9d63d..fab56ae6887 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import cast from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( @@ -43,9 +42,7 @@ async def async_setup_platform( if not discovery_info: return - discovery_info = cast(DiscoveryInfoType, discovery_info) binary_sensor_config = discovery_info - command: str = binary_sensor_config[CONF_COMMAND] payload_off: str = binary_sensor_config[CONF_PAYLOAD_OFF] payload_on: str = binary_sensor_config[CONF_PAYLOAD_ON] diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 8ddfd399ba8..7f1bc12264c 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from homeassistant.components.cover import CoverEntity from homeassistant.const import ( @@ -41,7 +41,6 @@ async def async_setup_platform( return covers = [] - discovery_info = cast(DiscoveryInfoType, discovery_info) entities: dict[str, dict[str, Any]] = { slugify(discovery_info[CONF_NAME]): discovery_info } diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 4f5a4e4b499..ec1b51a47c7 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import subprocess -from typing import Any, cast +from typing import Any from homeassistant.components.notify import BaseNotificationService from homeassistant.const import CONF_COMMAND @@ -26,7 +26,6 @@ def get_service( if not discovery_info: return None - discovery_info = cast(DiscoveryInfoType, discovery_info) notify_config = discovery_info command: str = notify_config[CONF_COMMAND] timeout: int = notify_config[CONF_COMMAND_TIMEOUT] diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index e4c1370d5f7..b7c36a005fa 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Mapping from datetime import datetime, timedelta import json -from typing import Any, cast +from typing import Any from jsonpath import jsonpath @@ -51,9 +51,7 @@ async def async_setup_platform( if not discovery_info: return - discovery_info = cast(DiscoveryInfoType, discovery_info) sensor_config = discovery_info - command: str = sensor_config[CONF_COMMAND] command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index e42c2226cf2..31400048ddc 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ( @@ -40,7 +40,6 @@ async def async_setup_platform( return switches = [] - discovery_info = cast(DiscoveryInfoType, discovery_info) entities: dict[str, dict[str, Any]] = { slugify(discovery_info[CONF_NAME]): discovery_info } diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py index ce3a0cf028d..5ff47977d88 100644 --- a/homeassistant/components/conversation/agent_manager.py +++ b/homeassistant/components/conversation/agent_manager.py @@ -79,6 +79,9 @@ async def async_converse( extra_system_prompt: str | None = None, ) -> ConversationResult: """Process text and get intent.""" + if agent_id is None: + agent_id = HOME_ASSISTANT_AGENT + agent = async_get_agent(hass, agent_id) if agent is None: diff --git a/homeassistant/components/conversation/chat_log.py b/homeassistant/components/conversation/chat_log.py index ad7a9d0ce9e..e4ff1904e7c 100644 --- a/homeassistant/components/conversation/chat_log.py +++ b/homeassistant/components/conversation/chat_log.py @@ -1,9 +1,11 @@ -"""Conversation history.""" +"""Conversation chat log.""" from __future__ import annotations +import asyncio from collections.abc import AsyncGenerator, Generator from contextlib import contextmanager +from contextvars import ContextVar from dataclasses import dataclass, field, replace import logging @@ -19,10 +21,14 @@ from . import trace from .const import DOMAIN from .models import ConversationInput, ConversationResult -DATA_CHAT_HISTORY: HassKey[dict[str, ChatLog]] = HassKey("conversation_chat_log") +DATA_CHAT_LOGS: HassKey[dict[str, ChatLog]] = HassKey("conversation_chat_logs") LOGGER = logging.getLogger(__name__) +current_chat_log: ContextVar[ChatLog | None] = ContextVar( + "current_chat_log", default=None +) + @contextmanager def async_get_chat_log( @@ -31,41 +37,50 @@ def async_get_chat_log( user_input: ConversationInput | None = None, ) -> Generator[ChatLog]: """Return chat log for a specific chat session.""" - all_history = hass.data.get(DATA_CHAT_HISTORY) - if all_history is None: - all_history = {} - hass.data[DATA_CHAT_HISTORY] = all_history + if chat_log := current_chat_log.get(): + # If a chat log is already active and it's the requested conversation ID, + # return that. We won't update the last updated time in this case. + if chat_log.conversation_id == session.conversation_id: + yield chat_log + return - history = all_history.get(session.conversation_id) + all_chat_logs = hass.data.get(DATA_CHAT_LOGS) + if all_chat_logs is None: + all_chat_logs = {} + hass.data[DATA_CHAT_LOGS] = all_chat_logs - if history: - history = replace(history, content=history.content.copy()) + chat_log = all_chat_logs.get(session.conversation_id) + + if chat_log: + chat_log = replace(chat_log, content=chat_log.content.copy()) else: - history = ChatLog(hass, session.conversation_id) + chat_log = ChatLog(hass, session.conversation_id) if user_input is not None: - history.async_add_user_content(UserContent(content=user_input.text)) + chat_log.async_add_user_content(UserContent(content=user_input.text)) - last_message = history.content[-1] + last_message = chat_log.content[-1] - yield history + token = current_chat_log.set(chat_log) + yield chat_log + current_chat_log.reset(token) - if history.content[-1] is last_message: + if chat_log.content[-1] is last_message: LOGGER.debug( - "History opened but no assistant message was added, ignoring update" + "Chat Log opened but no assistant message was added, ignoring update" ) return - if session.conversation_id not in all_history: + if session.conversation_id not in all_chat_logs: @callback def do_cleanup() -> None: """Handle cleanup.""" - all_history.pop(session.conversation_id) + all_chat_logs.pop(session.conversation_id) session.async_on_cleanup(do_cleanup) - all_history[session.conversation_id] = history + all_chat_logs[session.conversation_id] = chat_log class ConverseError(HomeAssistantError): @@ -112,7 +127,7 @@ class AssistantContent: role: str = field(init=False, default="assistant") agent_id: str - content: str + content: str | None = None tool_calls: list[llm.ToolInput] | None = None @@ -143,6 +158,7 @@ class ChatLog: @callback def async_add_user_content(self, content: UserContent) -> None: """Add user content to the log.""" + LOGGER.debug("Adding user content: %s", content) self.content.append(content) @callback @@ -150,14 +166,24 @@ class ChatLog: self, content: AssistantContent ) -> None: """Add assistant content to the log.""" + LOGGER.debug("Adding assistant content: %s", content) if content.tool_calls is not None: raise ValueError("Tool calls not allowed") self.content.append(content) async def async_add_assistant_content( - self, content: AssistantContent + self, + content: AssistantContent, + /, + tool_call_tasks: dict[str, asyncio.Task] | None = None, ) -> AsyncGenerator[ToolResultContent]: - """Add assistant content.""" + """Add assistant content and execute tool calls. + + tool_call_tasks can contains tasks for tool calls that are already in progress. + + This method is an async generator and will yield the tool results as they come in. + """ + LOGGER.debug("Adding assistant content: %s", content) self.content.append(content) if content.tool_calls is None: @@ -166,13 +192,22 @@ class ChatLog: if self.llm_api is None: raise ValueError("No LLM API configured") + if tool_call_tasks is None: + tool_call_tasks = {} + for tool_input in content.tool_calls: + if tool_input.id not in tool_call_tasks: + tool_call_tasks[tool_input.id] = self.hass.async_create_task( + self.llm_api.async_call_tool(tool_input), + name=f"llm_tool_{tool_input.id}", + ) + for tool_input in content.tool_calls: LOGGER.debug( "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args ) try: - tool_result = await self.llm_api.async_call_tool(tool_input) + tool_result = await tool_call_tasks[tool_input.id] except (HomeAssistantError, vol.Invalid) as e: tool_result = {"error": type(e).__name__} if str(e): diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index bd7450e5a0f..23c201d7579 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -381,7 +381,7 @@ class DefaultAgent(ConversationEntity): speech: str = response.speech.get("plain", {}).get("speech", "") chat_log.async_add_assistant_content_without_tools( AssistantContent( - agent_id=user_input.agent_id, # type: ignore[arg-type] + agent_id=user_input.agent_id, content=speech, ) ) diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index 8134ecb0eee..4d8526a4fd4 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -195,7 +195,7 @@ async def websocket_hass_agent_debug( conversation_id=None, device_id=msg.get("device_id"), language=msg.get("language", hass.config.language), - agent_id=None, + agent_id=agent.entity_id, ) result_dict: dict[str, Any] | None = None diff --git a/homeassistant/components/conversation/models.py b/homeassistant/components/conversation/models.py index 9462c597f23..08a68fa0164 100644 --- a/homeassistant/components/conversation/models.py +++ b/homeassistant/components/conversation/models.py @@ -37,7 +37,7 @@ class ConversationInput: language: str """Language of the request.""" - agent_id: str | None = None + agent_id: str """Agent to use for processing.""" extra_system_prompt: str | None = None diff --git a/homeassistant/components/discovergy/__init__.py b/homeassistant/components/discovergy/__init__.py index 81c33adc052..9cf63176de6 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -6,18 +6,15 @@ from pydiscovergy import Discovergy from pydiscovergy.authentication import BasicAuth import pydiscovergy.error as discovergyError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client -from .coordinator import DiscovergyUpdateCoordinator +from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator PLATFORMS = [Platform.SENSOR] -type DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]] - async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> bool: """Set up Discovergy from a config entry.""" @@ -46,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) - # so we have data when entities are added coordinator = DiscovergyUpdateCoordinator( hass=hass, + config_entry=entry, meter=meter, discovergy_client=client, ) diff --git a/homeassistant/components/discovergy/coordinator.py b/homeassistant/components/discovergy/coordinator.py index 3be4c71c987..d4ef87049b8 100644 --- a/homeassistant/components/discovergy/coordinator.py +++ b/homeassistant/components/discovergy/coordinator.py @@ -9,19 +9,25 @@ from pydiscovergy import Discovergy from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin from pydiscovergy.models import Meter, Reading +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed _LOGGER = logging.getLogger(__name__) +type DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]] + class DiscovergyUpdateCoordinator(DataUpdateCoordinator[Reading]): """The Discovergy update coordinator.""" + config_entry: DiscovergyConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: DiscovergyConfigEntry, meter: Meter, discovergy_client: Discovergy, ) -> None: @@ -32,6 +38,7 @@ class DiscovergyUpdateCoordinator(DataUpdateCoordinator[Reading]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"Discovergy meter {meter.meter_id}", update_interval=timedelta(seconds=30), ) diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index 3857404db81..f4d6a3397d0 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -8,7 +8,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.core import HomeAssistant -from . import DiscovergyConfigEntry +from .coordinator import DiscovergyConfigEntry TO_REDACT_METER = { "serial_number", diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index a3ec132db9b..65b1722e0d8 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -24,9 +24,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DiscovergyConfigEntry from .const import DOMAIN, MANUFACTURER -from .coordinator import DiscovergyUpdateCoordinator +from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/dremel_3d_printer/__init__.py b/homeassistant/components/dremel_3d_printer/__init__.py index 632c42d9b54..33a8ad0e67f 100644 --- a/homeassistant/components/dremel_3d_printer/__init__.py +++ b/homeassistant/components/dremel_3d_printer/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry( f"Unable to connect to Dremel 3D Printer: {ex}" ) from ex - coordinator = Dremel3DPrinterDataUpdateCoordinator(hass, api) + coordinator = Dremel3DPrinterDataUpdateCoordinator(hass, config_entry, api) await coordinator.async_config_entry_first_refresh() config_entry.runtime_data = coordinator platforms = list(PLATFORMS) diff --git a/homeassistant/components/dremel_3d_printer/coordinator.py b/homeassistant/components/dremel_3d_printer/coordinator.py index 3323569c05f..f2c1876fe0a 100644 --- a/homeassistant/components/dremel_3d_printer/coordinator.py +++ b/homeassistant/components/dremel_3d_printer/coordinator.py @@ -18,11 +18,14 @@ class Dremel3DPrinterDataUpdateCoordinator(DataUpdateCoordinator[None]): config_entry: DremelConfigEntry - def __init__(self, hass: HomeAssistant, api: Dremel3DPrinter) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: DremelConfigEntry, api: Dremel3DPrinter + ) -> None: """Initialize Dremel 3D Printer data update coordinator.""" super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=10), ) diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 7a56299a35b..727fcf95339 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry( device_registry = dr.async_get(hass) if device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}): device_registry.async_clear_config_entry(entry.entry_id) - coordinator = DwdWeatherWarningsCoordinator(hass) + coordinator = DwdWeatherWarningsCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py index be61304bc06..61656a82de6 100644 --- a/homeassistant/components/dwd_weather_warnings/coordinator.py +++ b/homeassistant/components/dwd_weather_warnings/coordinator.py @@ -28,10 +28,16 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): config_entry: DwdWeatherWarningsConfigEntry api: DwdWeatherWarningsAPI - def __init__(self, hass: HomeAssistant) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: DwdWeatherWarningsConfigEntry + ) -> None: """Initialize the dwd_weather_warnings coordinator.""" super().__init__( - hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + hass, + LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, ) self._device_tracker = None diff --git a/homeassistant/components/eheimdigital/__init__.py b/homeassistant/components/eheimdigital/__init__.py index a555a87cfbc..26e6bea4d4a 100644 --- a/homeassistant/components/eheimdigital/__init__.py +++ b/homeassistant/components/eheimdigital/__init__.py @@ -2,25 +2,22 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry from .const import DOMAIN -from .coordinator import EheimDigitalUpdateCoordinator +from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator PLATFORMS = [Platform.CLIMATE, Platform.LIGHT] -type EheimDigitalConfigEntry = ConfigEntry[EheimDigitalUpdateCoordinator] - async def async_setup_entry( hass: HomeAssistant, entry: EheimDigitalConfigEntry ) -> bool: """Set up EHEIM Digital from a config entry.""" - coordinator = EheimDigitalUpdateCoordinator(hass) + coordinator = EheimDigitalUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/eheimdigital/climate.py b/homeassistant/components/eheimdigital/climate.py index 7ad06659089..f0038982965 100644 --- a/homeassistant/components/eheimdigital/climate.py +++ b/homeassistant/components/eheimdigital/climate.py @@ -23,9 +23,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EheimDigitalConfigEntry from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE -from .coordinator import EheimDigitalUpdateCoordinator +from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .entity import EheimDigitalEntity # Coordinator is used to centralize the data updates diff --git a/homeassistant/components/eheimdigital/coordinator.py b/homeassistant/components/eheimdigital/coordinator.py index ee4f09426b7..4359a314494 100644 --- a/homeassistant/components/eheimdigital/coordinator.py +++ b/homeassistant/components/eheimdigital/coordinator.py @@ -22,18 +22,26 @@ type AsyncSetupDeviceEntitiesCallback = Callable[ [str | dict[str, EheimDigitalDevice]], None ] +type EheimDigitalConfigEntry = ConfigEntry[EheimDigitalUpdateCoordinator] + class EheimDigitalUpdateCoordinator( DataUpdateCoordinator[dict[str, EheimDigitalDevice]] ): """The EHEIM Digital data update coordinator.""" - config_entry: ConfigEntry + config_entry: EheimDigitalConfigEntry - def __init__(self, hass: HomeAssistant) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: EheimDigitalConfigEntry + ) -> None: """Initialize the EHEIM Digital data update coordinator.""" super().__init__( - hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + hass, + LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, ) self.hub = EheimDigitalHub( host=self.config_entry.data[CONF_HOST], diff --git a/homeassistant/components/eheimdigital/light.py b/homeassistant/components/eheimdigital/light.py index 5ae0a6e866a..25498cf3af1 100644 --- a/homeassistant/components/eheimdigital/light.py +++ b/homeassistant/components/eheimdigital/light.py @@ -19,9 +19,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import brightness_to_value, value_to_brightness -from . import EheimDigitalConfigEntry from .const import EFFECT_DAYCL_MODE, EFFECT_TO_LIGHT_MODE -from .coordinator import EheimDigitalUpdateCoordinator +from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .entity import EheimDigitalEntity BRIGHTNESS_SCALE = (1, 100) diff --git a/homeassistant/components/emoncms/__init__.py b/homeassistant/components/emoncms/__init__.py index 581948bbc6f..012abcc8c9a 100644 --- a/homeassistant/components/emoncms/__init__.py +++ b/homeassistant/components/emoncms/__init__.py @@ -2,7 +2,6 @@ from pyemoncms import EmoncmsClient -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -10,12 +9,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN, EMONCMS_UUID_DOC_URL, LOGGER -from .coordinator import EmoncmsCoordinator +from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -type EmonCMSConfigEntry = ConfigEntry[EmoncmsCoordinator] - def _migrate_unique_id( hass: HomeAssistant, entry: EmonCMSConfigEntry, emoncms_unique_id: str @@ -68,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> b session=async_get_clientsession(hass), ) await _check_unique_id_migration(hass, entry, emoncms_client) - coordinator = EmoncmsCoordinator(hass, emoncms_client) + coordinator = EmoncmsCoordinator(hass, entry, emoncms_client) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -77,11 +74,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> b return True -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: EmonCMSConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/emoncms/coordinator.py b/homeassistant/components/emoncms/coordinator.py index c6fda5ed7c8..ec439b400d5 100644 --- a/homeassistant/components/emoncms/coordinator.py +++ b/homeassistant/components/emoncms/coordinator.py @@ -5,24 +5,31 @@ from typing import Any from pyemoncms import EmoncmsClient +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_MESSAGE, CONF_SUCCESS, LOGGER +type EmonCMSConfigEntry = ConfigEntry[EmoncmsCoordinator] + class EmoncmsCoordinator(DataUpdateCoordinator[list[dict[str, Any]] | None]): """Emoncms Data Update Coordinator.""" + config_entry: EmonCMSConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: EmonCMSConfigEntry, emoncms_client: EmoncmsClient, ) -> None: """Initialize the emoncms data coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name="emoncms_coordinator", update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 1920e06a8e8..e3483d3f5d7 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -53,7 +53,7 @@ from .const import ( FEED_NAME, FEED_TAG, ) -from .coordinator import EmoncmsCoordinator +from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator SENSORS: dict[str | None, SensorEntityDescription] = { "kWh": SensorEntityDescription( @@ -288,7 +288,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: EmonCMSConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the emoncms sensors.""" diff --git a/homeassistant/components/fireservicerota/coordinator.py b/homeassistant/components/fireservicerota/coordinator.py index 35f839b3bdb..14a8c40e469 100644 --- a/homeassistant/components/fireservicerota/coordinator.py +++ b/homeassistant/components/fireservicerota/coordinator.py @@ -54,7 +54,9 @@ class FireServiceUpdateCoordinator(DataUpdateCoordinator[dict | None]): class FireServiceRotaOauth: """Handle authentication tokens.""" - def __init__(self, hass, entry, fsr): + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, fsr: FireServiceRota + ) -> None: """Initialize the oauth object.""" self._hass = hass self._entry = entry @@ -94,7 +96,7 @@ class FireServiceRotaOauth: class FireServiceRotaWebSocket: """Define a FireServiceRota websocket manager object.""" - def __init__(self, hass, entry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the websocket object.""" self._hass = hass self._entry = entry @@ -128,7 +130,7 @@ class FireServiceRotaWebSocket: class FireServiceRotaClient: """Getting the latest data from fireservicerota.""" - def __init__(self, hass, entry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the data object.""" self._hass = hass self._entry = entry diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 864838ddaff..b09d1295025 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN +from .coordinator import FireServiceRotaClient _LOGGER = logging.getLogger(__name__) @@ -32,13 +33,13 @@ class IncidentsSensor(RestoreEntity, SensorEntity): _attr_has_entity_name = True _attr_translation_key = "incidents" - def __init__(self, client): + def __init__(self, client: FireServiceRotaClient) -> None: """Initialize.""" self._client = client self._entry_id = self._client.entry_id self._attr_unique_id = f"{self._client.unique_id}_Incidents" - self._state = None - self._state_attributes = {} + self._state: str | None = None + self._state_attributes: dict[str, Any] = {} @property def icon(self) -> str: @@ -52,7 +53,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): return "mdi:fire-truck" @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 22287653788..affd46c91bd 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -10,6 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN +from .coordinator import FireServiceRotaClient, FireServiceUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,15 +33,20 @@ class ResponseSwitch(SwitchEntity): _attr_has_entity_name = True _attr_translation_key = "incident_response" - def __init__(self, coordinator, client, entry): + def __init__( + self, + coordinator: FireServiceUpdateCoordinator, + client: FireServiceRotaClient, + entry: ConfigEntry, + ) -> None: """Initialize.""" self._coordinator = coordinator self._client = client self._attr_unique_id = f"{entry.unique_id}_Response" self._entry_id = entry.entry_id - self._state = None - self._state_attributes = {} + self._state: bool | None = None + self._state_attributes: dict[str, Any] = {} self._state_icon = None @property @@ -54,7 +60,7 @@ class ResponseSwitch(SwitchEntity): return "mdi:forum" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get the assumed state of the switch.""" return self._state diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index 26fbe596aa8..45cae276967 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -122,6 +122,8 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [BOARD_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA ) +type FirmataConfigEntry = ConfigEntry[FirmataBoard] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Firmata domain.""" @@ -158,11 +160,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: FirmataConfigEntry +) -> bool: """Set up a Firmata board for a config entry.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - _LOGGER.debug( "Setting up Firmata id %s, name %s, config %s", config_entry.entry_id, @@ -175,13 +176,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if not await board.async_setup(): return False - hass.data[DOMAIN][config_entry.entry_id] = board + config_entry.runtime_data = board async def handle_shutdown(event) -> None: """Handle shutdown of board when Home Assistant shuts down.""" - # Ensure board was not already removed previously before shutdown - if config_entry.entry_id in hass.data[DOMAIN]: - await board.async_reset() + await board.async_reset() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) @@ -208,7 +207,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: FirmataConfigEntry +) -> bool: """Shutdown and close a Firmata board for a config entry.""" _LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME]) results: list[bool] = [] @@ -220,6 +221,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> results.append( await hass.config_entries.async_unload_platforms(config_entry, platforms) ) - results.append(await hass.data[DOMAIN].pop(config_entry.entry_id).async_reset()) + results.append(await config_entry.runtime_data.async_reset()) return False not in results diff --git a/homeassistant/components/firmata/binary_sensor.py b/homeassistant/components/firmata/binary_sensor.py index c25a61ddac7..4973afa6960 100644 --- a/homeassistant/components/firmata/binary_sensor.py +++ b/homeassistant/components/firmata/binary_sensor.py @@ -3,12 +3,12 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN +from . import FirmataConfigEntry +from .const import CONF_NEGATE_STATE, CONF_PIN_MODE from .entity import FirmataPinEntity from .pin import FirmataBinaryDigitalInput, FirmataPinUsedException @@ -17,13 +17,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FirmataConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Firmata binary sensors.""" new_entities = [] - board = hass.data[DOMAIN][config_entry.entry_id] + board = config_entry.runtime_data for binary_sensor in board.binary_sensors: pin = binary_sensor[CONF_PIN] pin_mode = binary_sensor[CONF_PIN_MODE] diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index 00453762c14..4f27143b774 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -11,8 +11,9 @@ from homeassistant.const import CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import FirmataConfigEntry from .board import FirmataPinType -from .const import CONF_INITIAL_STATE, CONF_PIN_MODE, DOMAIN +from .const import CONF_INITIAL_STATE, CONF_PIN_MODE from .entity import FirmataPinEntity from .pin import FirmataBoardPin, FirmataPinUsedException, FirmataPWMOutput @@ -21,13 +22,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FirmataConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Firmata lights.""" new_entities = [] - board = hass.data[DOMAIN][config_entry.entry_id] + board = config_entry.runtime_data for light in board.lights: pin = light[CONF_PIN] pin_mode = light[CONF_PIN_MODE] diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py index 32559b9197d..569d97fe1ec 100644 --- a/homeassistant/components/firmata/sensor.py +++ b/homeassistant/components/firmata/sensor.py @@ -3,12 +3,12 @@ import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN +from . import FirmataConfigEntry +from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE from .entity import FirmataPinEntity from .pin import FirmataAnalogInput, FirmataPinUsedException @@ -17,13 +17,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FirmataConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Firmata sensors.""" new_entities = [] - board = hass.data[DOMAIN][config_entry.entry_id] + board = config_entry.runtime_data for sensor in board.sensors: pin = sensor[CONF_PIN] pin_mode = sensor[CONF_PIN_MODE] diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py index 4203b221d4f..33953b78974 100644 --- a/homeassistant/components/firmata/switch.py +++ b/homeassistant/components/firmata/switch.py @@ -4,12 +4,12 @@ import logging from typing import Any from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_INITIAL_STATE, CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN +from . import FirmataConfigEntry +from .const import CONF_INITIAL_STATE, CONF_NEGATE_STATE, CONF_PIN_MODE from .entity import FirmataPinEntity from .pin import FirmataBinaryDigitalOutput, FirmataPinUsedException @@ -18,13 +18,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FirmataConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Firmata switches.""" new_entities = [] - board = hass.data[DOMAIN][config_entry.entry_id] + board = config_entry.runtime_data for switch in board.switches: pin = switch[CONF_PIN] pin_mode = switch[CONF_PIN_MODE] diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 25d24502846..c69a8172272 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -6,20 +6,18 @@ import logging from fivem import FiveMServerOfflineError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN -from .coordinator import FiveMDataUpdateCoordinator +from .coordinator import FiveMConfigEntry, FiveMDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FiveMConfigEntry) -> bool: """Set up FiveM from a config entry.""" _LOGGER.debug( "Create FiveM server instance for '%s:%s'", @@ -27,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PORT], ) - coordinator = FiveMDataUpdateCoordinator(hass, entry.data, entry.entry_id) + coordinator = FiveMDataUpdateCoordinator(hass, entry) try: await coordinator.initialize() @@ -36,16 +34,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: FiveMConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/fivem/binary_sensor.py b/homeassistant/components/fivem/binary_sensor.py index de58ea52fb6..42119939d4a 100644 --- a/homeassistant/components/fivem/binary_sensor.py +++ b/homeassistant/components/fivem/binary_sensor.py @@ -7,11 +7,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, NAME_STATUS +from .const import NAME_STATUS +from .coordinator import FiveMConfigEntry from .entity import FiveMEntity, FiveMEntityDescription @@ -33,11 +33,11 @@ BINARY_SENSORS: tuple[FiveMBinarySensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FiveMConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the FiveM binary sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( [FiveMSensorEntity(coordinator, description) for description in BINARY_SENSORS] diff --git a/homeassistant/components/fivem/coordinator.py b/homeassistant/components/fivem/coordinator.py index 1fdf87fb2b7..2fcad7e0c98 100644 --- a/homeassistant/components/fivem/coordinator.py +++ b/homeassistant/components/fivem/coordinator.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Mapping from datetime import timedelta import logging from typing import Any from fivem import FiveM, FiveMServerOfflineError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,26 +26,32 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type FiveMConfigEntry = ConfigEntry[FiveMDataUpdateCoordinator] + class FiveMDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching FiveM data.""" - def __init__( - self, hass: HomeAssistant, config_data: Mapping[str, Any], unique_id: str - ) -> None: + def __init__(self, hass: HomeAssistant, entry: FiveMConfigEntry) -> None: """Initialize server instance.""" - self.unique_id = unique_id + self.unique_id = entry.entry_id self.server = None self.version = None self.game_name: str | None = None - self.host = config_data[CONF_HOST] + self.host = entry.data[CONF_HOST] - self._fivem = FiveM(self.host, config_data[CONF_PORT]) + self._fivem = FiveM(self.host, entry.data[CONF_PORT]) update_interval = timedelta(seconds=SCAN_INTERVAL) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=DOMAIN, + update_interval=update_interval, + ) async def initialize(self) -> None: """Initialize the FiveM server.""" diff --git a/homeassistant/components/fivem/sensor.py b/homeassistant/components/fivem/sensor.py index b63f3b9082f..88290171756 100644 --- a/homeassistant/components/fivem/sensor.py +++ b/homeassistant/components/fivem/sensor.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -11,7 +10,6 @@ from homeassistant.helpers.typing import StateType from .const import ( ATTR_PLAYERS_LIST, ATTR_RESOURCES_LIST, - DOMAIN, NAME_PLAYERS_MAX, NAME_PLAYERS_ONLINE, NAME_RESOURCES, @@ -19,6 +17,7 @@ from .const import ( UNIT_PLAYERS_ONLINE, UNIT_RESOURCES, ) +from .coordinator import FiveMConfigEntry from .entity import FiveMEntity, FiveMEntityDescription @@ -50,11 +49,11 @@ SENSORS: tuple[FiveMSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FiveMConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the FiveM sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data # Add sensor entities. async_add_entities( diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index d91c6b175cf..24ed41b21c1 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -7,7 +7,7 @@ from requests import Session from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -24,22 +24,18 @@ from homeassistant.core import ( from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.selector import ConfigEntrySelector -from .const import ( - BASE_TOKEN_FILENAME, - DOMAIN, - FLUME_AUTH, - FLUME_DEVICES, - FLUME_HTTP_SESSION, - FLUME_NOTIFICATIONS_COORDINATOR, - PLATFORMS, +from .const import BASE_TOKEN_FILENAME, DOMAIN, PLATFORMS +from .coordinator import ( + FlumeConfigEntry, + FlumeNotificationDataUpdateCoordinator, + FlumeRuntimeData, ) -from .coordinator import FlumeNotificationDataUpdateCoordinator SERVICE_LIST_NOTIFICATIONS = "list_notifications" CONF_CONFIG_ENTRY = "config_entry" LIST_NOTIFICATIONS_SERVICE_SCHEMA = vol.All( { - vol.Required(CONF_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(CONF_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), }, ) @@ -76,7 +72,7 @@ def _setup_entry( return flume_auth, flume_devices, http_session -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FlumeConfigEntry) -> bool: """Set up flume from a config entry.""" flume_auth, flume_devices, http_session = await hass.async_add_executor_job( @@ -86,12 +82,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass=hass, auth=flume_auth ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - FLUME_DEVICES: flume_devices, - FLUME_AUTH: flume_auth, - FLUME_HTTP_SESSION: http_session, - FLUME_NOTIFICATIONS_COORDINATOR: notification_coordinator, - } + entry.runtime_data = FlumeRuntimeData( + devices=flume_devices, + auth=flume_auth, + http_session=http_session, + notifications_coordinator=notification_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) setup_service(hass) @@ -99,16 +95,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: FlumeConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - hass.data[DOMAIN][entry.entry_id][FLUME_HTTP_SESSION].close() - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + entry.runtime_data.http_session.close() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) def setup_service(hass: HomeAssistant) -> None: @@ -118,15 +108,13 @@ def setup_service(hass: HomeAssistant) -> None: def list_notifications(call: ServiceCall) -> ServiceResponse: """Return the user notifications.""" entry_id: str = call.data[CONF_CONFIG_ENTRY] - entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id) + entry: FlumeConfigEntry | None = hass.config_entries.async_get_entry(entry_id) if not entry: raise ValueError(f"Invalid config entry: {entry_id}") - if not (flume_domain_data := hass.data[DOMAIN].get(entry_id)): + if not entry.state == ConfigEntryState.LOADED: raise ValueError(f"Config entry not loaded: {entry_id}") return { - "notifications": flume_domain_data[ - FLUME_NOTIFICATIONS_COORDINATOR - ].notifications + "notifications": entry.runtime_data.notifications_coordinator.notifications # type: ignore[dict-item] } hass.services.async_register( diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 28f56168d9c..67cb71c5767 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -9,15 +9,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DOMAIN, - FLUME_DEVICES, - FLUME_NOTIFICATIONS_COORDINATOR, FLUME_TYPE_BRIDGE, FLUME_TYPE_SENSOR, KEY_DEVICE_ID, @@ -29,6 +25,7 @@ from .const import ( NOTIFICATION_LOW_BATTERY, ) from .coordinator import ( + FlumeConfigEntry, FlumeDeviceConnectionUpdateCoordinator, FlumeNotificationDataUpdateCoordinator, ) @@ -71,12 +68,12 @@ FLUME_BINARY_NOTIFICATION_SENSORS: tuple[FlumeBinarySensorEntityDescription, ... async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FlumeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Flume binary sensor..""" - flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] - flume_devices = flume_domain_data[FLUME_DEVICES] + flume_domain_data = config_entry.runtime_data + flume_devices = flume_domain_data.devices flume_entity_list: list[ FlumeNotificationBinarySensor | FlumeConnectionBinarySensor @@ -85,7 +82,7 @@ async def async_setup_entry( connection_coordinator = FlumeDeviceConnectionUpdateCoordinator( hass=hass, flume_devices=flume_devices ) - notification_coordinator = flume_domain_data[FLUME_NOTIFICATIONS_COORDINATOR] + notification_coordinator = flume_domain_data.notifications_coordinator flume_devices = get_valid_flume_devices(flume_devices) for device in flume_devices: device_id = device[KEY_DEVICE_ID] diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 1f9fc10b1b3..a8fe21f4b06 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -26,12 +26,6 @@ _LOGGER = logging.getLogger(__package__) FLUME_TYPE_BRIDGE = 1 FLUME_TYPE_SENSOR = 2 - -FLUME_AUTH = "flume_auth" -FLUME_HTTP_SESSION = "http_session" -FLUME_DEVICES = "devices" -FLUME_NOTIFICATIONS_COORDINATOR = "notifications_coordinator" - CONF_TOKEN_FILE = "token_filename" BASE_TOKEN_FILENAME = "FLUME_TOKEN_FILE" diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 30e7962304c..fc76600cad4 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -2,11 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any import pyflume from pyflume import FlumeAuth, FlumeData, FlumeDeviceList +from requests import Session +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,6 +22,19 @@ from .const import ( ) +@dataclass +class FlumeRuntimeData: + """Runtime data for the Flume config entry.""" + + devices: FlumeDeviceList + auth: FlumeAuth + http_session: Session + notifications_coordinator: FlumeNotificationDataUpdateCoordinator + + +type FlumeConfigEntry = ConfigEntry[FlumeRuntimeData] + + class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for an individual flume device.""" diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 96395e5403f..6c7cc0ab37d 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,10 +18,6 @@ from homeassistant.helpers.typing import StateType from .const import ( DEVICE_SCAN_INTERVAL, - DOMAIN, - FLUME_AUTH, - FLUME_DEVICES, - FLUME_HTTP_SESSION, FLUME_TYPE_SENSOR, KEY_DEVICE_ID, KEY_DEVICE_LOCATION, @@ -30,7 +25,7 @@ from .const import ( KEY_DEVICE_LOCATION_TIMEZONE, KEY_DEVICE_TYPE, ) -from .coordinator import FlumeDeviceDataUpdateCoordinator +from .coordinator import FlumeConfigEntry, FlumeDeviceDataUpdateCoordinator from .entity import FlumeEntity from .util import get_valid_flume_devices @@ -112,15 +107,15 @@ def make_flume_datas( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FlumeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Flume sensor.""" - flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] - flume_devices = flume_domain_data[FLUME_DEVICES] - flume_auth: FlumeAuth = flume_domain_data[FLUME_AUTH] - http_session: Session = flume_domain_data[FLUME_HTTP_SESSION] + flume_domain_data = config_entry.runtime_data + flume_devices = flume_domain_data.devices + flume_auth = flume_domain_data.auth + http_session = flume_domain_data.http_session flume_devices = [ device for device in get_valid_flume_devices(flume_devices) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 6ff9a030a02..2172e60ba38 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY +from .const import DOMAIN, HASS_DATA_UPDATER_KEY PLATFORMS = [Platform.MEDIA_PLAYER] @@ -23,10 +23,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: HASS_DATA_UPDATER_KEY ].websocket_handler: websocket_handler.cancel() - for remove_listener in hass.data[DOMAIN][entry.entry_id][ - HASS_DATA_REMOVE_LISTENERS_KEY - ]: - remove_listener() del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: del hass.data[DOMAIN] diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 8d671f2fc07..dd7ed1bdf16 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -32,7 +32,6 @@ DEFAULT_TTS_VOLUME = 0.8 DEFAULT_UNMUTE_VOLUME = 0.6 DOMAIN = "forked_daapd" # key for hass.data FD_NAME = "OwnTone" -HASS_DATA_REMOVE_LISTENERS_KEY = "REMOVE_LISTENERS" HASS_DATA_UPDATER_KEY = "UPDATER" KNOWN_PIPES = {"librespot-java"} PIPE_FUNCTION_MAP = { diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index b8b544c1a2c..0116cc57e7b 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -58,7 +58,6 @@ from .const import ( DEFAULT_UNMUTE_VOLUME, DOMAIN, FD_NAME, - HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY, KNOWN_PIPES, PIPE_FUNCTION_MAP, @@ -110,19 +109,16 @@ async def async_setup_entry( ForkedDaapdZone(api, output, config_entry.entry_id) for output in outputs ) - remove_add_zones_listener = async_dispatcher_connect( - hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones + config_entry.async_on_unload( + async_dispatcher_connect( + hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones + ) ) - remove_entry_listener = config_entry.add_update_listener(update_listener) + config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) if not hass.data.get(DOMAIN): hass.data[DOMAIN] = {config_entry.entry_id: {}} - hass.data[DOMAIN][config_entry.entry_id] = { - HASS_DATA_REMOVE_LISTENERS_KEY: [ - remove_add_zones_listener, - remove_entry_listener, - ] - } + async_add_entities([forked_daapd_master], False) forked_daapd_updater = ForkedDaapdUpdater( hass, forked_daapd_api, config_entry.entry_id diff --git a/homeassistant/components/frontier_silicon/__init__.py b/homeassistant/components/frontier_silicon/__init__.py index 325af100005..71196c13f68 100644 --- a/homeassistant/components/frontier_silicon/__init__.py +++ b/homeassistant/components/frontier_silicon/__init__.py @@ -11,14 +11,18 @@ from homeassistant.const import CONF_PIN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_WEBFSAPI_URL, DOMAIN +from .const import CONF_WEBFSAPI_URL PLATFORMS = [Platform.MEDIA_PLAYER] _LOGGER = logging.getLogger(__name__) +type FrontierSiliconConfigEntry = ConfigEntry[AFSAPI] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: FrontierSiliconConfigEntry +) -> bool: """Set up Frontier Silicon from a config entry.""" webfsapi_url = entry.data[CONF_WEBFSAPI_URL] @@ -31,16 +35,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except FSConnectionError as exception: raise ConfigEntryNotReady from exception - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = afsapi + entry.runtime_data = afsapi await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: FrontierSiliconConfigEntry +) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 52998e03703..6b0f987baa2 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -20,11 +20,11 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import FrontierSiliconConfigEntry from .browse_media import browse_node, browse_top_level from .const import DOMAIN, MEDIA_CONTENT_ID_PRESET @@ -33,12 +33,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FrontierSiliconConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Frontier Silicon entity.""" - afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id] + afsapi = config_entry.runtime_data async_add_entities( [ diff --git a/homeassistant/components/google_drive/application_credentials.py b/homeassistant/components/google_drive/application_credentials.py index 1c4421623d4..8bcab2b039c 100644 --- a/homeassistant/components/google_drive/application_credentials.py +++ b/homeassistant/components/google_drive/application_credentials.py @@ -2,7 +2,10 @@ from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.config_entry_oauth2_flow import ( + AUTH_CALLBACK_PATH, + MY_AUTH_CALLBACK_PATH, +) async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: @@ -15,9 +18,14 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: """Return description placeholders for the credentials dialog.""" + if "my" in hass.config.components: + redirect_url = MY_AUTH_CALLBACK_PATH + else: + ha_host = hass.config.external_url or "https://YOUR_DOMAIN:PORT" + redirect_url = f"{ha_host}{AUTH_CALLBACK_PATH}" return { "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", "more_info_url": "https://www.home-assistant.io/integrations/google_drive/", "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", - "redirect_url": config_entry_oauth2_flow.async_get_redirect_uri(hass), + "redirect_url": redirect_url, } diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 8a6c5563601..0f26c93da25 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -261,8 +261,6 @@ class GoogleGenerativeAIConversationEntity( chat_log: conversation.ChatLog, ) -> conversation.ConversationResult: """Call the API.""" - - assert user_input.agent_id options = self.entry.options try: diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index 942db675b5a..faf1ff1ee0b 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -39,7 +39,7 @@ SERVICE_APPEND_SHEET = "append_sheet" SHEET_SERVICE_SCHEMA = vol.All( { - vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), vol.Optional(WORKSHEET): cv.string, vol.Required(DATA): vol.Any(cv.ensure_list, [dict]), }, diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index d735469c5cb..0c268b612ea 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -39,9 +39,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool ): for domain, player_id in device.identifiers: if domain == DOMAIN and not isinstance(player_id, str): - device_registry.async_update_device( # type: ignore[unreachable] - device.id, new_identifiers={(DOMAIN, str(player_id))} - ) + # Create set of identifiers excluding this integration + identifiers = { # type: ignore[unreachable] + (domain, identifier) + for domain, identifier in device.identifiers + if domain != DOMAIN + } + migrated_identifiers = {(DOMAIN, str(player_id))} + # Add migrated if not already present in another device, which occurs if the user downgraded and then upgraded + if not device_registry.async_get_device(migrated_identifiers): + identifiers.update(migrated_identifiers) + if len(identifiers) > 0: + device_registry.async_update_device( + device.id, new_identifiers=identifiers + ) + else: + device_registry.async_remove_device(device.id) break coordinator = HeosCoordinator(hass, entry) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index b483df75d75..de328a54bb7 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -96,7 +96,7 @@ }, "notify_unknown_multipan_user": { "title": "Manual configuration may be needed", - "description": "Home Assistant can automatically change the channels for otbr and zha. If you have configured another integration to use the radio, for example Zigbee2MQTT, you will have to reconfigure the channel in that integration after completing this guide." + "description": "Home Assistant can automatically change the channels for OTBR and ZHA. If you have configured another integration to use the radio, for example Zigbee2MQTT, you will have to reconfigure the channel in that integration after completing this guide." }, "reconfigure_addon": { "title": "Reconfigure IEEE 802.15.4 radio multiprotocol support" @@ -131,7 +131,7 @@ "addon_already_running": "Failed to start the {addon_name} add-on because it is already running.", "addon_set_config_failed": "Failed to set {addon_name} configuration.", "addon_start_failed": "Failed to start the {addon_name} add-on.", - "not_hassio": "The hardware options can only be configured on HassOS installations.", + "not_hassio": "The hardware options can only be configured on Home Assistant OS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, "progress": { diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 5ec02a86709..8d7e44ecd99 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -10,8 +10,8 @@ from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import SCAN_INTERVAL @@ -26,6 +26,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): name: str id: str hass: HomeAssistant + devices: list[Sensor] | None = None def __init__( self, @@ -60,24 +61,34 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): except LoginError as error: raise ConfigEntryAuthFailed from error + if self.devices is None: + _LOGGER.debug("Getting devices") + try: + self.devices = await self.api.get_devices( + location=Location(id=self.id, name=self.name), + ) + except HTTPError as error: + raise UpdateFailed from error + try: # Fetch last hour of data - sensors = await self.api.get_sensors( - location=Location(id=self.id, name=self.name), - tz=self.hass.config.time_zone, - start=str(now - 3600), - end=str(now), - ) - except HTTPError as error: - raise ConfigEntryNotReady from error + for sensor in self.devices: + sensor.data = ( + await self.api.get_sensor_status( + sensor=sensor, + tz=self.hass.config.time_zone, + ) + )["data"]["current"] + _LOGGER.debug("Got data: %s", sensor.data) - _LOGGER.debug("Got data: %s", sensors) + except HTTPError as error: + raise UpdateFailed from error # Verify that we have permission to read the sensors - for sensor in sensors: + for sensor in self.devices: if not sensor.permissions.get("read", False): raise ConfigEntryAuthFailed( f"This account does not have permission to read {sensor.name}" ) - return sensors + return self.devices diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index fceddeb9b2c..5c56a0328a2 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -48,7 +48,7 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: field_data = sensor.data.get(field) if sensor.data is not None else None if field_data is None: return None - value = field_data["values"][-1]["s"] + value = field_data["spot"]["value"] try: value = float(value) except ValueError: diff --git a/homeassistant/components/letpot/__init__.py b/homeassistant/components/letpot/__init__.py index 5bfc4edfc0c..bc84c22d4a2 100644 --- a/homeassistant/components/letpot/__init__.py +++ b/homeassistant/components/letpot/__init__.py @@ -9,7 +9,6 @@ from letpot.converters import CONVERTERS from letpot.exceptions import LetPotAuthenticationException, LetPotException from letpot.models import AuthenticationInfo -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -21,12 +20,10 @@ from .const import ( CONF_REFRESH_TOKEN_EXPIRES, CONF_USER_ID, ) -from .coordinator import LetPotDeviceCoordinator +from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.TIME] -type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]] - async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bool: """Set up LetPot from a config entry.""" @@ -67,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bo raise ConfigEntryNotReady from exc coordinators: list[LetPotDeviceCoordinator] = [ - LetPotDeviceCoordinator(hass, auth, device) + LetPotDeviceCoordinator(hass, entry, auth, device) for device in devices if any(converter.supports_type(device.device_type) for converter in CONVERTERS) ] diff --git a/homeassistant/components/letpot/coordinator.py b/homeassistant/components/letpot/coordinator.py index a2a35d566c6..bd787157482 100644 --- a/homeassistant/components/letpot/coordinator.py +++ b/homeassistant/components/letpot/coordinator.py @@ -4,23 +4,22 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING from letpot.deviceclient import LetPotDeviceClient from letpot.exceptions import LetPotAuthenticationException, LetPotException from letpot.models import AuthenticationInfo, LetPotDevice, LetPotDeviceStatus +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import REQUEST_UPDATE_TIMEOUT -if TYPE_CHECKING: - from . import LetPotConfigEntry - _LOGGER = logging.getLogger(__name__) +type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]] + class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]): """Class to handle data updates for a specific garden.""" @@ -31,12 +30,17 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]): device_client: LetPotDeviceClient def __init__( - self, hass: HomeAssistant, info: AuthenticationInfo, device: LetPotDevice + self, + hass: HomeAssistant, + config_entry: LetPotConfigEntry, + info: AuthenticationInfo, + device: LetPotDevice, ) -> None: """Initialize coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"LetPot {device.serial_number}", ) self._info = info diff --git a/homeassistant/components/letpot/switch.py b/homeassistant/components/letpot/switch.py index 36d07276c48..ab02f2860c6 100644 --- a/homeassistant/components/letpot/switch.py +++ b/homeassistant/components/letpot/switch.py @@ -12,8 +12,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LetPotConfigEntry -from .coordinator import LetPotDeviceCoordinator +from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator from .entity import LetPotEntity, exception_handler # Each change pushes a 'full' device status with the change. The library will cache diff --git a/homeassistant/components/letpot/time.py b/homeassistant/components/letpot/time.py index 80ce9743d8c..cca088c8e61 100644 --- a/homeassistant/components/letpot/time.py +++ b/homeassistant/components/letpot/time.py @@ -13,8 +13,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LetPotConfigEntry -from .coordinator import LetPotDeviceCoordinator +from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator from .entity import LetPotEntity, exception_handler # Each change pushes a 'full' device status with the change. The library will cache diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 8f498e0f8a2..dee2d21e05a 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -215,9 +215,9 @@ "error_during_washing": "An error has occurred in the washing machine", "error_has_occurred": "An error has occurred", "frozen_is_complete": "Ice plus is done", - "homeguard_is_stopped": "Home guard has stopped", + "homeguard_is_stopped": "Home Guard has stopped", "lack_of_water": "There is no water in the water tank", - "motion_is_detected": "Photograph is sent as movement is detected during home guard", + "motion_is_detected": "Photograph is sent as movement is detected during Home Guard", "need_to_check_location": "Location check is required", "pollution_is_high": "Air status is rapidly becoming bad", "preheating_is_complete": "Preheating is done", @@ -432,9 +432,9 @@ "lock": "Control lock", "macrosector": "Remote is in use", "melting": "Wort dissolving", - "monitoring_detecting": "HomeGuard is active", + "monitoring_detecting": "Home Guard is active", "monitoring_moving": "Going to the starting point", - "monitoring_positioning": "Setting homeguard start point", + "monitoring_positioning": "Setting Home Guard start point", "night_dry": "Night dry", "oven_setting": "Cooktop connected", "pause": "[%key:common::state::paused%]", diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index ca8c28f9d13..15e3348adbe 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -128,7 +128,7 @@ def setup_services(hass: HomeAssistant) -> None: translation_domain=DOMAIN, translation_key="end_date_before_start_date", ) - client = cast(MealieConfigEntry, entry).runtime_data.client + client = entry.runtime_data.client try: mealplans = await client.get_mealplans(start_date, end_date) except MealieConnectionError as err: diff --git a/homeassistant/components/motionmount/manifest.json b/homeassistant/components/motionmount/manifest.json index 422be417006..2665836ffd4 100644 --- a/homeassistant/components/motionmount/manifest.json +++ b/homeassistant/components/motionmount/manifest.json @@ -6,6 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/motionmount", "integration_type": "device", "iot_class": "local_push", + "quality_scale": "bronze", "requirements": ["python-MotionMount==2.3.0"], "zeroconf": ["_tvm._tcp.local."] } diff --git a/homeassistant/components/motionmount/quality_scale.yaml b/homeassistant/components/motionmount/quality_scale.yaml new file mode 100644 index 00000000000..e4a6a04ceeb --- /dev/null +++ b/homeassistant/components/motionmount/quality_scale.yaml @@ -0,0 +1,78 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: Integration does not have actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: done + comment: Integration does register actions aside from entity actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: Integration does not register events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: Integration does not have actions + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: Integration has no options flow + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: todo + reauthentication-flow: done + test-coverage: todo + + # Gold + devices: done + diagnostics: todo + discovery-update-info: done + discovery: done + docs-data-update: done + docs-examples: todo + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: Single device per config entry + entity-category: todo + entity-device-class: todo + entity-disabled-by-default: todo + entity-translations: done + exception-translations: done + icon-translations: todo + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: Integration does not need user intervention + stale-devices: + status: exempt + comment: Integration does not support dynamic devices + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: Device doesn't make http requests. + strict-typing: done diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 4b4c026260d..7af7465bbd0 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "homekit": { - "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"] + "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59", "NL69", "NL81"] }, "iot_class": "local_push", "loggers": ["aionanoleaf"], @@ -22,6 +22,12 @@ }, { "st": "nanoleaf:nl52" + }, + { + "st": "nanoleaf:nl69" + }, + { + "st": "inanoleaf:nl81" } ], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."] diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 3c7393aa184..d34a5abe8af 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import partial from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Literal, Required, TypedDict, cast +from typing import Any, Required, TypedDict, cast import voluptuous as vol @@ -177,8 +177,6 @@ class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]]) for forecast_type in ("twice_daily", "hourly"): if (coordinator := self.forecast_coordinators[forecast_type]) is None: continue - if TYPE_CHECKING: - forecast_type = cast(Literal["twice_daily", "hourly"], forecast_type) self.unsub_forecast[forecast_type] = coordinator.async_add_listener( partial(self._handle_forecast_update, forecast_type) ) diff --git a/homeassistant/components/onedrive/__init__.py b/homeassistant/components/onedrive/__init__.py index 5feefb2cf7d..8355cddb0b5 100644 --- a/homeassistant/components/onedrive/__init__.py +++ b/homeassistant/components/onedrive/__init__.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass +from html import unescape +from json import dumps, loads import logging from typing import cast @@ -13,6 +15,7 @@ from onedrive_personal_sdk.exceptions import ( HttpRequestException, OneDriveException, ) +from onedrive_personal_sdk.models.items import ItemUpdate from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN @@ -45,7 +48,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) -> bool: """Set up OneDrive from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) - session = OAuth2Session(hass, entry, implementation) async def get_access_token() -> str: @@ -89,6 +91,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) -> backup_folder_id=backup_folder.id, ) + try: + await _migrate_backup_files(client, backup_folder.id) + except OneDriveException as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="failed_to_migrate_files", + ) from err + _async_notify_backup_listeners_soon(hass) return True @@ -108,3 +118,34 @@ def _async_notify_backup_listeners(hass: HomeAssistant) -> None: @callback def _async_notify_backup_listeners_soon(hass: HomeAssistant) -> None: hass.loop.call_soon(_async_notify_backup_listeners, hass) + + +async def _migrate_backup_files(client: OneDriveClient, backup_folder_id: str) -> None: + """Migrate backup files to metadata version 2.""" + files = await client.list_drive_items(backup_folder_id) + for file in files: + if file.description and '"metadata_version": 1' in ( + metadata_json := unescape(file.description) + ): + metadata = loads(metadata_json) + del metadata["metadata_version"] + metadata_filename = file.name.rsplit(".", 1)[0] + ".metadata.json" + metadata_file = await client.upload_file( + backup_folder_id, + metadata_filename, + dumps(metadata), + ) + metadata_description = { + "metadata_version": 2, + "backup_id": metadata["backup_id"], + "backup_file_id": file.id, + } + await client.update_drive_item( + path_or_id=metadata_file.id, + data=ItemUpdate(description=dumps(metadata_description)), + ) + await client.update_drive_item( + path_or_id=file.id, + data=ItemUpdate(description=""), + ) + _LOGGER.debug("Migrated backup file %s", file.name) diff --git a/homeassistant/components/onedrive/backup.py b/homeassistant/components/onedrive/backup.py index 78bdcb24b8c..9926bd9cbc7 100644 --- a/homeassistant/components/onedrive/backup.py +++ b/homeassistant/components/onedrive/backup.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import AsyncIterator, Callable, Coroutine from functools import wraps -import html -import json +from html import unescape +from json import dumps, loads import logging from typing import Any, Concatenate @@ -34,6 +34,7 @@ from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN _LOGGER = logging.getLogger(__name__) UPLOAD_CHUNK_SIZE = 16 * 320 * 1024 # 5.2MB TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours +METADATA_VERSION = 2 async def async_get_backup_agents( @@ -120,11 +121,19 @@ class OneDriveBackupAgent(BackupAgent): self, backup_id: str, **kwargs: Any ) -> AsyncIterator[bytes]: """Download a backup file.""" - item = await self._find_item_by_backup_id(backup_id) - if item is None: + metadata_item = await self._find_item_by_backup_id(backup_id) + if ( + metadata_item is None + or metadata_item.description is None + or "backup_file_id" not in metadata_item.description + ): raise BackupAgentError("Backup not found") - stream = await self._client.download_drive_item(item.id, timeout=TIMEOUT) + metadata_info = loads(unescape(metadata_item.description)) + + stream = await self._client.download_drive_item( + metadata_info["backup_file_id"], timeout=TIMEOUT + ) return stream.iter_chunked(1024) @handle_backup_errors @@ -136,15 +145,15 @@ class OneDriveBackupAgent(BackupAgent): **kwargs: Any, ) -> None: """Upload a backup.""" - + filename = suggested_filename(backup) file = FileInfo( - suggested_filename(backup), + filename, backup.size, self._folder_id, await open_stream(), ) try: - item = await LargeFileUploadClient.upload( + backup_file = await LargeFileUploadClient.upload( self._token_function, file, session=async_get_clientsession(self._hass) ) except HashMismatchError as err: @@ -152,15 +161,25 @@ class OneDriveBackupAgent(BackupAgent): "Hash validation failed, backup file might be corrupt" ) from err - # store metadata in description - backup_dict = backup.as_dict() - backup_dict["metadata_version"] = 1 # version of the backup metadata - description = json.dumps(backup_dict) + # store metadata in metadata file + description = dumps(backup.as_dict()) _LOGGER.debug("Creating metadata: %s", description) + metadata_filename = filename.rsplit(".", 1)[0] + ".metadata.json" + metadata_file = await self._client.upload_file( + self._folder_id, + metadata_filename, + description, + ) + # add metadata to the metadata file + metadata_description = { + "metadata_version": METADATA_VERSION, + "backup_id": backup.backup_id, + "backup_file_id": backup_file.id, + } await self._client.update_drive_item( - path_or_id=item.id, - data=ItemUpdate(description=description), + path_or_id=metadata_file.id, + data=ItemUpdate(description=dumps(metadata_description)), ) @handle_backup_errors @@ -170,18 +189,28 @@ class OneDriveBackupAgent(BackupAgent): **kwargs: Any, ) -> None: """Delete a backup file.""" - item = await self._find_item_by_backup_id(backup_id) - if item is None: + metadata_item = await self._find_item_by_backup_id(backup_id) + if ( + metadata_item is None + or metadata_item.description is None + or "backup_file_id" not in metadata_item.description + ): return - await self._client.delete_drive_item(item.id) + metadata_info = loads(unescape(metadata_item.description)) + + await self._client.delete_drive_item(metadata_info["backup_file_id"]) + await self._client.delete_drive_item(metadata_item.id) @handle_backup_errors async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]: """List backups.""" + items = await self._client.list_drive_items(self._folder_id) return [ - self._backup_from_description(item.description) - for item in await self._client.list_drive_items(self._folder_id) - if item.description and "homeassistant_version" in item.description + await self._download_backup_metadata(item.id) + for item in items + if item.description + and "backup_id" in item.description + and f'"metadata_version": {METADATA_VERSION}' in unescape(item.description) ] @handle_backup_errors @@ -189,19 +218,11 @@ class OneDriveBackupAgent(BackupAgent): self, backup_id: str, **kwargs: Any ) -> AgentBackup | None: """Return a backup.""" - item = await self._find_item_by_backup_id(backup_id) - return ( - self._backup_from_description(item.description) - if item and item.description - else None - ) + metadata_file = await self._find_item_by_backup_id(backup_id) + if metadata_file is None or metadata_file.description is None: + return None - def _backup_from_description(self, description: str) -> AgentBackup: - """Create a backup object from a description.""" - description = html.unescape( - description - ) # OneDrive encodes the description on save automatically - return AgentBackup.from_dict(json.loads(description)) + return await self._download_backup_metadata(metadata_file.id) async def _find_item_by_backup_id(self, backup_id: str) -> File | Folder | None: """Find an item by backup ID.""" @@ -209,7 +230,15 @@ class OneDriveBackupAgent(BackupAgent): ( item for item in await self._client.list_drive_items(self._folder_id) - if item.description and backup_id in item.description + if item.description + and backup_id in item.description + and f'"metadata_version": {METADATA_VERSION}' + in unescape(item.description) ), None, ) + + async def _download_backup_metadata(self, item_id: str) -> AgentBackup: + metadata_stream = await self._client.download_drive_item(item_id) + metadata_json = loads(await metadata_stream.read()) + return AgentBackup.from_dict(metadata_json) diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index 88d51e6d73a..fcc922b3e46 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_polling", "loggers": ["onedrive_personal_sdk"], "quality_scale": "bronze", - "requirements": ["onedrive-personal-sdk==0.0.8"] + "requirements": ["onedrive-personal-sdk==0.0.9"] } diff --git a/homeassistant/components/onedrive/strings.json b/homeassistant/components/onedrive/strings.json index 7686e83e2a5..ebc46d3eb12 100644 --- a/homeassistant/components/onedrive/strings.json +++ b/homeassistant/components/onedrive/strings.json @@ -35,6 +35,9 @@ }, "failed_to_get_folder": { "message": "Failed to get {folder} folder" + }, + "failed_to_migrate_files": { + "message": "Failed to migrate metadata to separate files" } } } diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 73dafa1c48d..eaa62bd1adc 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -198,7 +198,6 @@ class OpenAIConversationEntity( chat_log: conversation.ChatLog, ) -> conversation.ConversationResult: """Call the API.""" - assert user_input.agent_id options = self.entry.options try: diff --git a/homeassistant/components/opower/__init__.py b/homeassistant/components/opower/__init__.py index 136a1a4e57a..b8e4f4381d0 100644 --- a/homeassistant/components/opower/__init__.py +++ b/homeassistant/components/opower/__init__.py @@ -2,18 +2,14 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .coordinator import OpowerCoordinator +from .coordinator import OpowerConfigEntry, OpowerCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -type OpowerConfigEntry = ConfigEntry[OpowerCoordinator] - - async def async_setup_entry(hass: HomeAssistant, entry: OpowerConfigEntry) -> bool: """Set up Opower from a config entry.""" diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 6957ae4984c..8d7ef1ace94 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -23,6 +23,7 @@ from homeassistant.components.recorder.statistics import ( get_last_statistics, statistics_during_period, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed @@ -34,10 +35,14 @@ from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN _LOGGER = logging.getLogger(__name__) +type OpowerConfigEntry = ConfigEntry[OpowerCoordinator] + class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): """Handle fetching Opower data, updating sensors and inserting statistics.""" + config_entry: OpowerConfigEntry + def __init__( self, hass: HomeAssistant, @@ -59,6 +64,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): entry_data[CONF_PASSWORD], entry_data.get(CONF_TOTP_SECRET), ) + self._statistic_ids: set[str] = set() @callback def _dummy_listener() -> None: @@ -70,6 +76,12 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): # _async_update_data not periodically getting called which is needed for _insert_statistics. self.async_add_listener(_dummy_listener) + self.config_entry.async_on_unload(self._clear_statistics) + + def _clear_statistics(self) -> None: + """Clear statistics.""" + get_instance(self.hass).async_clear_statistics(list(self._statistic_ids)) + async def _async_update_data( self, ) -> dict[str, Forecast]: @@ -115,6 +127,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost" consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption" + self._statistic_ids.add(cost_statistic_id) + self._statistic_ids.add(consumption_statistic_id) _LOGGER.debug( "Updating Statistics for %s and %s", cost_statistic_id, diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index f9d0fe62332..1b3aa0fd710 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -20,9 +20,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import OpowerConfigEntry from .const import DOMAIN -from .coordinator import OpowerCoordinator +from .coordinator import OpowerConfigEntry, OpowerCoordinator @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 7cef284ef60..0b8532bedea 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "quality_scale": "internal", "requirements": [ - "SQLAlchemy==2.0.37", + "SQLAlchemy==2.0.38", "fnv-hash-fast==1.2.2", "psutil-home-assistant==0.0.1" ] diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index c8fa72606d6..d47f2b0ae80 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -28,6 +28,7 @@ from aioshelly.const import ( ) from homeassistant.components.number import NumberMode +from homeassistant.components.sensor import SensorDeviceClass DOMAIN: Final = "shelly" @@ -272,3 +273,8 @@ COMPONENT_ID_PATTERN = re.compile(r"[a-z\d]+:\d+") # value confirmed by Shelly team BLU_TRV_TIMEOUT = 60 + +ROLE_TO_DEVICE_CLASS_MAP = { + "current_humidity": SensorDeviceClass.HUMIDITY, + "current_temperature": SensorDeviceClass.TEMPERATURE, +} diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index c4420783bbb..1fc47b23bdb 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -139,6 +139,24 @@ class RpcBluTrvNumber(RpcNumber): ) +class RpcBluTrvExtTempNumber(RpcBluTrvNumber): + """Represent a RPC BluTrv External Temperature number.""" + + _reported_value: float | None = None + + @property + def native_value(self) -> float | None: + """Return value of number.""" + return self._reported_value + + async def async_set_native_value(self, value: float) -> None: + """Change the value.""" + await super().async_set_native_value(value) + + self._reported_value = value + self.async_write_ha_state() + + NUMBERS: dict[tuple[str, str], BlockNumberDescription] = { ("device", "valvePos"): BlockNumberDescription( key="device|valvepos", @@ -175,7 +193,7 @@ RPC_NUMBERS: Final = { "method": "Trv.SetExternalTemperature", "params": {"id": 0, "t_C": value}, }, - entity_class=RpcBluTrvNumber, + entity_class=RpcBluTrvExtTempNumber, ), "number": RpcNumberDescription( key="number", diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 6d000556cf3..c492fc1de9e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Final, cast @@ -38,7 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType -from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS +from .const import CONF_SLEEP_PERIOD, ROLE_TO_DEVICE_CLASS_MAP, SHAIR_MAX_WORK_HOURS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, @@ -71,6 +72,8 @@ class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription): class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): """Class to describe a RPC sensor.""" + device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None + @dataclass(frozen=True, kw_only=True) class RestSensorDescription(RestEntityDescription, SensorEntityDescription): @@ -95,6 +98,12 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity): if self.option_map: self._attr_options = list(self.option_map.values()) + if description.device_class_fn is not None: + if device_class := description.device_class_fn( + coordinator.device.config[key] + ): + self._attr_device_class = device_class + @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -1266,6 +1275,9 @@ RPC_SENSORS: Final = { unit=lambda config: config["meta"]["ui"]["unit"] if config["meta"]["ui"]["unit"] else None, + device_class_fn=lambda config: ROLE_TO_DEVICE_CLASS_MAP.get(config["role"]) + if "role" in config + else None, ), "enum": RpcSensorDescription( key="enum", diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 0094770d53b..c18b1b9f05f 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sql", "iot_class": "local_polling", - "requirements": ["SQLAlchemy==2.0.37", "sqlparse==0.5.0"] + "requirements": ["SQLAlchemy==2.0.38", "sqlparse==0.5.0"] } diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index fe44bc39e62..9c101204dcb 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -45,7 +45,7 @@ } }, "encrypted_choose_method": { - "description": "An encrypted SwitchBot device can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", + "description": "An encrypted SwitchBot device can be set up in Home Assistant in two different ways.\n\nYou can enter the key ID and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", "menu_options": { "encrypted_auth": "SwitchBot account (recommended)", "encrypted_key": "Enter encryption key manually" @@ -53,7 +53,7 @@ } }, "error": { - "encryption_key_invalid": "Key ID or Encryption key is invalid", + "encryption_key_invalid": "Key ID or encryption key is invalid", "auth_failed": "Authentication failed: {error_detail}" }, "abort": { @@ -61,7 +61,7 @@ "no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.", "unknown": "[%key:common::config_flow::error::unknown%]", "api_error": "Error while communicating with SwitchBot API: {error_detail}", - "switchbot_unsupported_type": "Unsupported Switchbot Type." + "switchbot_unsupported_type": "Unsupported SwitchBot type." } }, "options": { diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 856a0c5402b..b83e2695137 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.18.5"] + "requirements": ["python-tado==0.18.6"] } diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 721b491bbf5..68f22b51c47 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -55,7 +55,7 @@ }, "assignee": { "name": "Assignee", - "description": "A members username of a shared project to assign this task to." + "description": "The username of a shared project's member to assign this task to." }, "priority": { "name": "Priority", @@ -63,11 +63,11 @@ }, "due_date_string": { "name": "Due date string", - "description": "The day this task is due, in natural language." + "description": "The time this task is due, in natural language." }, "due_date_lang": { "name": "Due date language", - "description": "The language of due_date_string." + "description": "The language of 'Due date string'." }, "due_date": { "name": "Due date", @@ -75,15 +75,15 @@ }, "reminder_date_string": { "name": "Reminder date string", - "description": "When should user be reminded of this task, in natural language." + "description": "When the user should be reminded of this task, in natural language." }, "reminder_date_lang": { "name": "Reminder date language", - "description": "The language of reminder_date_string." + "description": "The language of 'Reminder date string'." }, "reminder_date": { "name": "Reminder date", - "description": "When should user be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone." + "description": "When the user should be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone." } } } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 076b6caad24..5c47a5e775f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -125,7 +125,7 @@ def cmd[_R, **_P]( self: LgWebOSMediaPlayerEntity, *args: _P.args, **kwargs: _P.kwargs ) -> _R: """Wrap all command methods.""" - if self.state is MediaPlayerState.OFF: + if self.state is MediaPlayerState.OFF and func.__name__ != "async_turn_off": raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_off", diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index d41ae7dbfee..b98e53f98d8 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -113,9 +113,14 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: except HomeAssistantError: pass else: - yellow_radio = next(p for p in ports if p.device == "/dev/ttyAMA1") - yellow_radio.description = "Yellow Zigbee module" - yellow_radio.manufacturer = "Nabu Casa" + # PySerial does not properly handle the Yellow's serial port with the CM5 + # so we manually include it + port = ListPortInfo(device="/dev/ttyAMA1", skip_link_detection=True) + port.description = "Yellow Zigbee module" + port.manufacturer = "Nabu Casa" + + ports = [p for p in ports if not p.device.startswith("/dev/ttyAMA")] + ports.insert(0, port) if is_hassio(hass): # Present the multi-PAN addon as a setup option, if it's available diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 89d1aa30cb8..5bbc178ba17 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -211,6 +211,12 @@ SSDP = { { "st": "nanoleaf:nl52", }, + { + "st": "nanoleaf:nl69", + }, + { + "st": "inanoleaf:nl81", + }, ], "netgear": [ { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8244f19660f..ab965e27472 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -208,6 +208,14 @@ HOMEKIT = { "always_discover": False, "domain": "nanoleaf", }, + "NL69": { + "always_discover": False, + "domain": "nanoleaf", + }, + "NL81": { + "always_discover": False, + "domain": "nanoleaf", + }, "Netatmo Relay": { "always_discover": True, "domain": "netatmo", diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 975b4a2aec9..92101dd0e21 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from datetime import datetime from enum import StrEnum from functools import lru_cache @@ -561,6 +561,21 @@ class DeviceRegistryItems[_EntryTypeT: (DeviceEntry, DeletedDeviceEntry)]( return self._connections[connection] return None + def get_entries( + self, + identifiers: set[tuple[str, str]] | None, + connections: set[tuple[str, str]] | None, + ) -> Iterable[_EntryTypeT]: + """Get entries from identifiers or connections.""" + if identifiers: + for identifier in identifiers: + if identifier in self._identifiers: + yield self._identifiers[identifier] + if connections: + for connection in _normalize_connections(connections): + if connection in self._connections: + yield self._connections[connection] + class ActiveDeviceRegistryItems(DeviceRegistryItems[DeviceEntry]): """Container for active (non-deleted) device registry entries.""" @@ -667,6 +682,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): """Check if device is deleted.""" return self.deleted_devices.get_entry(identifiers, connections) + def _async_get_deleted_devices( + self, + identifiers: set[tuple[str, str]] | None = None, + connections: set[tuple[str, str]] | None = None, + ) -> Iterable[DeletedDeviceEntry]: + """List devices that are deleted.""" + return self.deleted_devices.get_entries(identifiers, connections) + def _substitute_name_placeholders( self, domain: str, @@ -958,6 +981,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values["config_entries"] = config_entries old_values["config_entries"] = old.config_entries + added_connections: set[tuple[str, str]] | None = None + added_identifiers: set[tuple[str, str]] | None = None + if merge_connections is not UNDEFINED: normalized_connections = self._validate_connections( device_id, @@ -966,6 +992,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) old_connections = old.connections if not normalized_connections.issubset(old_connections): + added_connections = normalized_connections new_values["connections"] = old_connections | normalized_connections old_values["connections"] = old_connections @@ -975,17 +1002,18 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) old_identifiers = old.identifiers if not merge_identifiers.issubset(old_identifiers): + added_identifiers = merge_identifiers new_values["identifiers"] = old_identifiers | merge_identifiers old_values["identifiers"] = old_identifiers if new_connections is not UNDEFINED: - new_values["connections"] = self._validate_connections( + added_connections = new_values["connections"] = self._validate_connections( device_id, new_connections, False ) old_values["connections"] = old.connections if new_identifiers is not UNDEFINED: - new_values["identifiers"] = self._validate_identifiers( + added_identifiers = new_values["identifiers"] = self._validate_identifiers( device_id, new_identifiers, False ) old_values["identifiers"] = old.identifiers @@ -1028,6 +1056,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new = attr.evolve(old, **new_values) self.devices[device_id] = new + # NOTE: Once we solve the broader issue of duplicated devices, we might + # want to revisit it. Instead of simply removing the duplicated deleted device, + # we might want to merge the information from it into the non-deleted device. + for deleted_device in self._async_get_deleted_devices( + added_identifiers, added_connections + ): + del self.deleted_devices[deleted_device.id] + # If its only run time attributes (suggested_area) # that do not get saved we do not want to write # to disk or fire an event as we would end up diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 05c05d93548..0f53b732c13 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -61,7 +61,7 @@ PyTurboJPEG==1.7.5 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 standard-aifc==3.13.0 standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 diff --git a/pyproject.toml b/pyproject.toml index c6c506dbff7..3936fdb3a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ "PyYAML==6.0.2", "requests==2.32.3", "securetar==2025.1.4", - "SQLAlchemy==2.0.37", + "SQLAlchemy==2.0.38", "standard-aifc==3.13.0", "standard-telnetlib==3.13.0", "typing-extensions>=4.12.2,<5.0", diff --git a/requirements.txt b/requirements.txt index bd54a380fd3..f0ff3b8054a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ python-slugify==8.0.4 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 standard-aifc==3.13.0 standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4c3c2b42424..fe7ea7fc5ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,7 +116,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 @@ -1559,7 +1559,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.8 +onedrive-personal-sdk==0.0.9 # homeassistant.components.onvif onvif-zeep-async==3.2.5 @@ -2464,7 +2464,7 @@ python-smarttub==0.0.38 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.5 +python-tado==0.18.6 # homeassistant.components.technove python-technove==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef9b2978459..a686bbd0633 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -110,7 +110,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 @@ -1307,7 +1307,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.8 +onedrive-personal-sdk==0.0.9 # homeassistant.components.onvif onvif-zeep-async==3.2.5 @@ -1997,7 +1997,7 @@ python-smarttub==0.0.38 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.5 +python-tado==0.18.6 # homeassistant.components.technove python-technove==1.3.1 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index a1ad52e6aa8..e5eee2f4157 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -669,7 +669,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ "motion_blinds", "motionblinds_ble", "motioneye", - "motionmount", "mpd", "mqtt_eventstream", "mqtt_json", @@ -1748,7 +1747,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "motion_blinds", "motionblinds_ble", "motioneye", - "motionmount", "mpd", "mqtt_eventstream", "mqtt_json", diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 0a8f5b874fa..6faabc924b4 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -16,10 +16,12 @@ from amberelectric.models.spike_status import SpikeStatus from dateutil import parser import pytest +from homeassistant.components.amberelectric.const import CONF_SITE_ID, CONF_SITE_NAME from homeassistant.components.amberelectric.coordinator import ( AmberUpdateCoordinator, normalize_descriptor, ) +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed @@ -33,6 +35,17 @@ from .helpers import ( generate_current_interval, ) +from tests.common import MockConfigEntry + +MOCKED_ENTRY = MockConfigEntry( + domain="amberelectric", + data={ + CONF_SITE_NAME: "mock_title", + CONF_API_TOKEN: "psk_0000000000000000", + CONF_SITE_ID: GENERAL_ONLY_SITE_ID, + }, +) + @pytest.fixture(name="current_price_api") def mock_api_current_price() -> Generator: @@ -101,7 +114,9 @@ async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) """Test fetching a site with only a general channel.""" current_price_api.get_current_prices.return_value = GENERAL_CHANNEL - data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) + data_service = AmberUpdateCoordinator( + hass, MOCKED_ENTRY, current_price_api, GENERAL_ONLY_SITE_ID + ) result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( @@ -130,7 +145,9 @@ async def test_fetch_no_general_site( """Test fetching a site with no general channel.""" current_price_api.get_current_prices.return_value = CONTROLLED_LOAD_CHANNEL - data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) + data_service = AmberUpdateCoordinator( + hass, MOCKED_ENTRY, current_price_api, GENERAL_ONLY_SITE_ID + ) with pytest.raises(UpdateFailed): await data_service._async_update_data() @@ -143,7 +160,9 @@ async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) -> """Test that the old values are maintained if a second call fails.""" current_price_api.get_current_prices.return_value = GENERAL_CHANNEL - data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) + data_service = AmberUpdateCoordinator( + hass, MOCKED_ENTRY, current_price_api, GENERAL_ONLY_SITE_ID + ) result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( @@ -193,7 +212,7 @@ async def test_fetch_general_and_controlled_load_site( GENERAL_CHANNEL + CONTROLLED_LOAD_CHANNEL ) data_service = AmberUpdateCoordinator( - hass, current_price_api, GENERAL_AND_CONTROLLED_SITE_ID + hass, MOCKED_ENTRY, current_price_api, GENERAL_AND_CONTROLLED_SITE_ID ) result = await data_service._async_update_data() @@ -233,7 +252,7 @@ async def test_fetch_general_and_feed_in_site( GENERAL_CHANNEL + FEED_IN_CHANNEL ) data_service = AmberUpdateCoordinator( - hass, current_price_api, GENERAL_AND_FEED_IN_SITE_ID + hass, MOCKED_ENTRY, current_price_api, GENERAL_AND_FEED_IN_SITE_ID ) result = await data_service._async_update_data() @@ -273,7 +292,9 @@ async def test_fetch_potential_spike( ] general_channel[0].actual_instance.spike_status = SpikeStatus.POTENTIAL current_price_api.get_current_prices.return_value = general_channel - data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) + data_service = AmberUpdateCoordinator( + hass, MOCKED_ENTRY, current_price_api, GENERAL_ONLY_SITE_ID + ) result = await data_service._async_update_data() assert result["grid"]["price_spike"] == "potential" @@ -288,6 +309,8 @@ async def test_fetch_spike(hass: HomeAssistant, current_price_api: Mock) -> None ] general_channel[0].actual_instance.spike_status = SpikeStatus.SPIKE current_price_api.get_current_prices.return_value = general_channel - data_service = AmberUpdateCoordinator(hass, current_price_api, GENERAL_ONLY_SITE_ID) + data_service = AmberUpdateCoordinator( + hass, MOCKED_ENTRY, current_price_api, GENERAL_ONLY_SITE_ID + ) result = await data_service._async_update_data() assert result["grid"]["price_spike"] == "spike" diff --git a/tests/components/conversation/test_chat_log.py b/tests/components/conversation/test_chat_log.py index c22a90e6928..1f659b8005e 100644 --- a/tests/components/conversation/test_chat_log.py +++ b/tests/components/conversation/test_chat_log.py @@ -15,7 +15,7 @@ from homeassistant.components.conversation import ( ToolResultContent, async_get_chat_log, ) -from homeassistant.components.conversation.chat_log import DATA_CHAT_HISTORY +from homeassistant.components.conversation.chat_log import DATA_CHAT_LOGS from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import chat_session, llm @@ -63,7 +63,7 @@ async def test_cleanup( ) ) - assert conversation_id in hass.data[DATA_CHAT_HISTORY] + assert conversation_id in hass.data[DATA_CHAT_LOGS] # Set the last updated to be older than the timeout hass.data[chat_session.DATA_CHAT_SESSION][conversation_id].last_updated = ( @@ -75,7 +75,7 @@ async def test_cleanup( dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT * 2 + timedelta(seconds=1), ) - assert conversation_id not in hass.data[DATA_CHAT_HISTORY] + assert conversation_id not in hass.data[DATA_CHAT_LOGS] async def test_default_content( @@ -279,9 +279,18 @@ async def test_extra_systen_prompt( assert chat_log.content[0].content.endswith(extra_system_prompt2) +@pytest.mark.parametrize( + "prerun_tool_tasks", + [ + None, + ("mock-tool-call-id",), + ("mock-tool-call-id", "mock-tool-call-id-2"), + ], +) async def test_tool_call( hass: HomeAssistant, mock_conversation_input: ConversationInput, + prerun_tool_tasks: tuple[str] | None, ) -> None: """Test using the session tool calling API.""" @@ -316,26 +325,47 @@ async def test_tool_call( id="mock-tool-call-id", tool_name="test_tool", tool_args={"param1": "Test Param"}, - ) + ), + llm.ToolInput( + id="mock-tool-call-id-2", + tool_name="test_tool", + tool_args={"param1": "Test Param"}, + ), ], ) + tool_call_tasks = None + if prerun_tool_tasks: + tool_call_tasks = { + tool_call_id: hass.async_create_task( + chat_log.llm_api.async_call_tool(content.tool_calls[0]), + tool_call_id, + ) + for tool_call_id in prerun_tool_tasks + } + with pytest.raises(ValueError): chat_log.async_add_assistant_content_without_tools(content) - result = None - async for tool_result_content in chat_log.async_add_assistant_content( - content - ): - assert result is None - result = tool_result_content + results = [ + tool_result_content + async for tool_result_content in chat_log.async_add_assistant_content( + content, tool_call_tasks=tool_call_tasks + ) + ] - assert result == ToolResultContent( - agent_id=mock_conversation_input.agent_id, - tool_call_id="mock-tool-call-id", - tool_result="Test response", - tool_name="test_tool", - ) + assert results[0] == ToolResultContent( + agent_id=mock_conversation_input.agent_id, + tool_call_id="mock-tool-call-id", + tool_result="Test response", + tool_name="test_tool", + ) + assert results[1] == ToolResultContent( + agent_id=mock_conversation_input.agent_id, + tool_call_id="mock-tool-call-id-2", + tool_result="Test response", + tool_name="test_tool", + ) async def test_tool_call_exception( diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 6900ba2d419..9ac5c7d16a4 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -271,6 +271,7 @@ async def test_async_handle_sentence_triggers( text="my trigger", context=Context(), conversation_id=None, + agent_id=conversation.HOME_ASSISTANT_AGENT, device_id=device_id, language=hass.config.language, ), @@ -306,6 +307,7 @@ async def test_async_handle_intents(hass: HomeAssistant) -> None: ConversationInput( text="I'd like to order a stout", context=Context(), + agent_id=conversation.HOME_ASSISTANT_AGENT, conversation_id=None, device_id=None, language=hass.config.language, @@ -321,6 +323,7 @@ async def test_async_handle_intents(hass: HomeAssistant) -> None: hass, ConversationInput( text="this sentence does not exist", + agent_id=conversation.HOME_ASSISTANT_AGENT, context=Context(), conversation_id=None, device_id=None, diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 9b57bb43b58..3aa8ae2939f 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -5,7 +5,7 @@ import logging import pytest import voluptuous as vol -from homeassistant.components.conversation import default_agent +from homeassistant.components.conversation import HOME_ASSISTANT_AGENT, default_agent from homeassistant.components.conversation.const import DATA_DEFAULT_ENTITY from homeassistant.components.conversation.models import ConversationInput from homeassistant.core import Context, HomeAssistant, ServiceCall @@ -82,7 +82,7 @@ async def test_if_fires_on_event( "details": {}, "device_id": None, "user_input": { - "agent_id": None, + "agent_id": HOME_ASSISTANT_AGENT, "context": context.as_dict(), "conversation_id": None, "device_id": None, @@ -230,7 +230,7 @@ async def test_response_same_sentence( "details": {}, "device_id": None, "user_input": { - "agent_id": None, + "agent_id": HOME_ASSISTANT_AGENT, "context": context.as_dict(), "conversation_id": None, "device_id": None, @@ -408,7 +408,7 @@ async def test_same_trigger_multiple_sentences( "details": {}, "device_id": None, "user_input": { - "agent_id": None, + "agent_id": HOME_ASSISTANT_AGENT, "context": context.as_dict(), "conversation_id": None, "device_id": None, @@ -636,7 +636,7 @@ async def test_wildcards(hass: HomeAssistant, service_calls: list[ServiceCall]) }, "device_id": None, "user_input": { - "agent_id": None, + "agent_id": HOME_ASSISTANT_AGENT, "context": context.as_dict(), "conversation_id": None, "device_id": None, diff --git a/tests/components/google_drive/test_application_credentials.py b/tests/components/google_drive/test_application_credentials.py new file mode 100644 index 00000000000..ec46db510a5 --- /dev/null +++ b/tests/components/google_drive/test_application_credentials.py @@ -0,0 +1,36 @@ +"""Test the Google Drive application_credentials.""" + +import pytest + +from homeassistant import setup +from homeassistant.components.google_drive.application_credentials import ( + async_get_description_placeholders, +) +from homeassistant.core import HomeAssistant + + +@pytest.mark.parametrize( + ("additional_components", "external_url", "expected_redirect_uri"), + [ + ([], "https://example.com", "https://example.com/auth/external/callback"), + ([], None, "https://YOUR_DOMAIN:PORT/auth/external/callback"), + (["my"], "https://example.com", "https://my.home-assistant.io/redirect/oauth"), + ], +) +async def test_description_placeholders( + hass: HomeAssistant, + additional_components: list[str], + external_url: str | None, + expected_redirect_uri: str, +) -> None: + """Test description placeholders.""" + for component in additional_components: + assert await setup.async_setup_component(hass, component, {}) + hass.config.external_url = external_url + placeholders = await async_get_description_placeholders(hass) + assert placeholders == { + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", + "more_info_url": "https://www.home-assistant.io/integrations/google_drive/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + "redirect_url": expected_redirect_uri, + } diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 27dea82dcf2..81acb7b3b8b 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -193,13 +193,36 @@ async def test_device_id_migration( # Create a device with a legacy identifier device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, 1)}, # type: ignore[arg-type] + identifiers={(DOMAIN, 1), ("Other", "1")}, # type: ignore[arg-type] ) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={("Other", 1)}, # type: ignore[arg-type] ) assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) assert device_registry.async_get_device({("Other", 1)}) is not None # type: ignore[arg-type] assert device_registry.async_get_device({(DOMAIN, 1)}) is None # type: ignore[arg-type] assert device_registry.async_get_device({(DOMAIN, "1")}) is not None + assert device_registry.async_get_device({("Other", "1")}) is not None + + +async def test_device_id_migration_both_present( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, +) -> None: + """Test that legacy non-string devices are removed when both devices present.""" + config_entry.add_to_hass(hass) + # Create a device with a legacy identifier AND a new identifier + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, 1)}, # type: ignore[arg-type] + ) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "1")} + ) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + assert device_registry.async_get_device({(DOMAIN, 1)}) is None # type: ignore[arg-type] + assert device_registry.async_get_device({(DOMAIN, "1")}) is not None diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index 913f6c72f24..860156beb6c 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -15,7 +15,13 @@ TEST_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -26,7 +32,13 @@ TEST_NO_PERMISSION_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": False}, model="Test", ) @@ -37,7 +49,16 @@ TEST_UNSUPPORTED_SENSOR = Sensor( sensor_id="2", sensor_field_names=["SomeUnsupportedField"], location=Location(id="1", name="Test"), - data={"SomeUnsupportedField": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "SomeUnsupportedField": { + "spot": {"value": "2"}, + "unit": "degrees_celsius", + } + } + } + }, permissions={"read": True}, model="Test", ) @@ -48,7 +69,13 @@ TEST_FLOAT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2.3"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2.3"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -59,7 +86,9 @@ TEST_STRING_SENSOR = Sensor( sensor_id="2", sensor_field_names=["WetDry"], location=Location(id="1", name="Test"), - data={"WetDry": {"values": [{"s": "dry"}], "unit": "wet_dry"}}, + data={ + "data": {"current": {"WetDry": {"spot": {"value": "dry"}, "unit": "wet_dry"}}} + }, permissions={"read": True}, model="Test", ) @@ -70,7 +99,13 @@ TEST_ALREADY_FLOAT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["HeatIndex"], location=Location(id="1", name="Test"), - data={"HeatIndex": {"values": [{"s": 2.3}], "unit": "degrees_fahrenheit"}}, + data={ + "data": { + "current": { + "HeatIndex": {"spot": {"value": 2.3}, "unit": "degrees_fahrenheit"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -81,7 +116,13 @@ TEST_ALREADY_INT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["WindSpeed"], location=Location(id="1", name="Test"), - data={"WindSpeed": {"values": [{"s": 2}], "unit": "kilometers_per_hour"}}, + data={ + "data": { + "current": { + "WindSpeed": {"spot": {"value": 2}, "unit": "kilometers_per_hour"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -92,7 +133,7 @@ TEST_NO_FIELD_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={}, + data={"data": {"current": {}}}, permissions={"read": True}, model="Test", ) @@ -103,7 +144,7 @@ TEST_MISSING_FIELD_DATA_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": None}, + data={"data": {"current": {"Temperature": None}}}, permissions={"read": True}, model="Test", ) @@ -114,7 +155,13 @@ TEST_UNITS_OVERRIDE_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2.1"}], "unit": "degrees_fahrenheit"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2.1"}, "unit": "degrees_fahrenheit"} + } + } + }, permissions={"read": True}, model="Test", ) diff --git a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr index 201bbbc971e..bfbfa2901a6 100644 --- a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr +++ b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr @@ -4,7 +4,7 @@ 'coordinator_data': list([ dict({ '__type': "", - 'repr': "Sensor(name='Test', device_id='1', type='Test', sensor_id='2', sensor_field_names=['Temperature'], location=Location(id='1', name='Test'), permissions={'read': True}, model='Test', data={'Temperature': {'values': [{'s': '2'}], 'unit': 'degrees_celsius'}})", + 'repr': "Sensor(name='Test', device_id='1', type='Test', sensor_id='2', sensor_field_names=['Temperature'], location=Location(id='1', name='Test'), permissions={'read': True}, model='Test', data={'Temperature': {'spot': {'value': '2'}, 'unit': 'degrees_celsius'}})", }), ]), 'entry': dict({ diff --git a/tests/components/lacrosse_view/test_diagnostics.py b/tests/components/lacrosse_view/test_diagnostics.py index dc48f160113..4306173c6b3 100644 --- a/tests/components/lacrosse_view/test_diagnostics.py +++ b/tests/components/lacrosse_view/test_diagnostics.py @@ -26,9 +26,14 @@ async def test_entry_diagnostics( ) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 51fa7e5abf4..af92d0e64f1 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -20,12 +20,17 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -68,7 +73,7 @@ async def test_http_error(hass: HomeAssistant) -> None: with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError), + patch("lacrosse_view.LaCrosse.get_devices", side_effect=HTTPError), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -84,12 +89,17 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -103,7 +113,7 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", + "lacrosse_view.LaCrosse.get_devices", return_value=[TEST_SENSOR], ), ): @@ -121,12 +131,17 @@ async def test_failed_token( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 11faaf8877e..74e9f001792 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -32,9 +32,14 @@ async def test_entities_added(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -54,12 +59,17 @@ async def test_sensor_permission( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_NO_PERMISSION_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_NO_PERMISSION_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -79,11 +89,14 @@ async def test_field_not_supported( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_UNSUPPORTED_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] - ), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -114,12 +127,17 @@ async def test_field_types( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = test_input.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", + "lacrosse_view.LaCrosse.get_devices", return_value=[test_input], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -137,12 +155,17 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_NO_FIELD_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_NO_FIELD_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -160,12 +183,17 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_MISSING_FIELD_DATA_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_MISSING_FIELD_DATA_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json index e1893c30b42..7dea4405fc5 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json @@ -13,13 +13,6 @@ "hardware_serial": -1, "software_serial": -1, "hardware_type": -1 - }, - { - "address": [0, 5, true], - "name": "TestGroup", - "hardware_serial": -1, - "software_serial": -1, - "hardware_type": -1 } ], "entities": [ @@ -33,216 +26,6 @@ "dimmable": true, "transition": 5000.0 } - }, - { - "address": [0, 7, false], - "name": "Light_Output2", - "resource": "output2", - "domain": "light", - "domain_data": { - "output": "OUTPUT2", - "dimmable": false, - "transition": 0 - } - }, - { - "address": [0, 7, false], - "name": "Light_Relay1", - "resource": "relay1", - "domain": "light", - "domain_data": { - "output": "RELAY1", - "dimmable": false, - "transition": 0.0 - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output1", - "resource": "output1", - "domain": "switch", - "domain_data": { - "output": "OUTPUT1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output2", - "resource": "output2", - "domain": "switch", - "domain_data": { - "output": "OUTPUT2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay1", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay2", - "resource": "relay2", - "domain": "switch", - "domain_data": { - "output": "RELAY2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Regulator1", - "resource": "r1varsetpoint", - "domain": "switch", - "domain_data": { - "output": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Switch_KeyLock1", - "resource": "a1", - "domain": "switch", - "domain_data": { - "output": "A1" - } - }, - { - "address": [0, 5, true], - "name": "Switch_Group5", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Outputs", - "resource": "outputs", - "domain": "cover", - "domain_data": { - "motor": "OUTPUTS", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Relays", - "resource": "motor1", - "domain": "cover", - "domain_data": { - "motor": "MOTOR1", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Climate1", - "resource": "var1.r1varsetpoint", - "domain": "climate", - "domain_data": { - "source": "VAR1", - "setpoint": "R1VARSETPOINT", - "lockable": true, - "min_temp": 0.0, - "max_temp": 40.0, - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Romantic", - "resource": "0.0", - "domain": "scene", - "domain_data": { - "register": 0, - "scene": 0, - "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": null - } - }, - { - "address": [0, 7, false], - "name": "Romantic Transition", - "resource": "0.1", - "domain": "scene", - "domain_data": { - "register": 0, - "scene": 1, - "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": 10000 - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LockRegulator1", - "resource": "r1varsetpoint", - "domain": "binary_sensor", - "domain_data": { - "source": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Binary_Sensor1", - "resource": "binsensor1", - "domain": "binary_sensor", - "domain_data": { - "source": "BINSENSOR1" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_KeyLock", - "resource": "a5", - "domain": "binary_sensor", - "domain_data": { - "source": "A5" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Var1", - "resource": "var1", - "domain": "sensor", - "domain_data": { - "source": "VAR1", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Setpoint1", - "resource": "r1varsetpoint", - "domain": "sensor", - "domain_data": { - "source": "R1VARSETPOINT", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Led6", - "resource": "led6", - "domain": "sensor", - "domain_data": { - "source": "LED6", - "unit_of_measurement": "NATIVE" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LogicOp1", - "resource": "logicop1", - "domain": "sensor", - "domain_data": { - "source": "LOGICOP1", - "unit_of_measurement": "NATIVE" - } } ] } diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json index 7389079dca9..4cade6b64d0 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json @@ -14,13 +14,6 @@ "hardware_serial": -1, "software_serial": -1, "hardware_type": -1 - }, - { - "address": [0, 5, true], - "name": "TestGroup", - "hardware_serial": -1, - "software_serial": -1, - "hardware_type": -1 } ], "entities": [ @@ -43,115 +36,7 @@ "domain_data": { "output": "OUTPUT2", "dimmable": false, - "transition": 0 - } - }, - { - "address": [0, 7, false], - "name": "Light_Relay1", - "resource": "relay1", - "domain": "light", - "domain_data": { - "output": "RELAY1", - "dimmable": false, - "transition": 0.0 - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output1", - "resource": "output1", - "domain": "switch", - "domain_data": { - "output": "OUTPUT1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output2", - "resource": "output2", - "domain": "switch", - "domain_data": { - "output": "OUTPUT2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay1", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay2", - "resource": "relay2", - "domain": "switch", - "domain_data": { - "output": "RELAY2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Regulator1", - "resource": "r1varsetpoint", - "domain": "switch", - "domain_data": { - "output": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Switch_KeyLock1", - "resource": "a1", - "domain": "switch", - "domain_data": { - "output": "A1" - } - }, - { - "address": [0, 5, true], - "name": "Switch_Group5", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Outputs", - "resource": "outputs", - "domain": "cover", - "domain_data": { - "motor": "OUTPUTS", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Relays", - "resource": "motor1", - "domain": "cover", - "domain_data": { - "motor": "MOTOR1", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Climate1", - "resource": "var1.r1varsetpoint", - "domain": "climate", - "domain_data": { - "source": "VAR1", - "setpoint": "R1VARSETPOINT", - "lockable": true, - "min_temp": 0.0, - "max_temp": 40.0, - "unit_of_measurement": "°C" + "transition": null } }, { @@ -177,73 +62,6 @@ "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], "transition": 10000 } - }, - { - "address": [0, 7, false], - "name": "Sensor_LockRegulator1", - "resource": "r1varsetpoint", - "domain": "binary_sensor", - "domain_data": { - "source": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Binary_Sensor1", - "resource": "binsensor1", - "domain": "binary_sensor", - "domain_data": { - "source": "BINSENSOR1" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_KeyLock", - "resource": "a5", - "domain": "binary_sensor", - "domain_data": { - "source": "A5" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Var1", - "resource": "var1", - "domain": "sensor", - "domain_data": { - "source": "VAR1", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Setpoint1", - "resource": "r1varsetpoint", - "domain": "sensor", - "domain_data": { - "source": "R1VARSETPOINT", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Led6", - "resource": "led6", - "domain": "sensor", - "domain_data": { - "source": "LED6", - "unit_of_measurement": "NATIVE" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LogicOp1", - "resource": "logicop1", - "domain": "sensor", - "domain_data": { - "source": "LOGICOP1", - "unit_of_measurement": "NATIVE" - } } ] } diff --git a/tests/components/lcn/snapshots/test_init.ambr b/tests/components/lcn/snapshots/test_init.ambr new file mode 100644 index 00000000000..ea6267aaa0b --- /dev/null +++ b/tests/components/lcn/snapshots/test_init.ambr @@ -0,0 +1,140 @@ +# serializer version: 1 +# name: test_migrate_1_1 + dict({ + 'acknowledge': False, + 'devices': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'hardware_serial': -1, + 'hardware_type': -1, + 'name': 'TestModule', + 'software_serial': -1, + }), + ]), + 'dim_mode': 'STEPS200', + 'entities': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': True, + 'output': 'OUTPUT1', + 'transition': 5.0, + }), + 'name': 'Light_Output1', + 'resource': 'output1', + }), + ]), + 'host': 'pchk', + 'ip_address': '192.168.2.41', + 'password': 'lcn', + 'port': 4114, + 'sk_num_tries': 0, + 'username': 'lcn', + }) +# --- +# name: test_migrate_1_2 + dict({ + 'acknowledge': False, + 'devices': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'hardware_serial': -1, + 'hardware_type': -1, + 'name': 'TestModule', + 'software_serial': -1, + }), + ]), + 'dim_mode': 'STEPS200', + 'entities': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': True, + 'output': 'OUTPUT1', + 'transition': 5.0, + }), + 'name': 'Light_Output1', + 'resource': 'output1', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': False, + 'output': 'OUTPUT2', + 'transition': 0.0, + }), + 'name': 'Light_Output2', + 'resource': 'output2', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'scene', + 'domain_data': dict({ + 'outputs': list([ + 'OUTPUT1', + 'OUTPUT2', + 'RELAY1', + ]), + 'register': 0, + 'scene': 0, + 'transition': 0.0, + }), + 'name': 'Romantic', + 'resource': '0.0', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'scene', + 'domain_data': dict({ + 'outputs': list([ + 'OUTPUT1', + 'OUTPUT2', + 'RELAY1', + ]), + 'register': 0, + 'scene': 1, + 'transition': 10.0, + }), + 'name': 'Romantic Transition', + 'resource': '0.1', + }), + ]), + 'host': 'pchk', + 'ip_address': '192.168.2.41', + 'password': 'lcn', + 'port': 4114, + 'sk_num_tries': 0, + 'username': 'lcn', + }) +# --- diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 4bb8d023d3f..ef3c2d3cb66 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -11,6 +11,7 @@ from pypck.connection import ( ) from pypck.lcn_defs import LcnEvent import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.lcn.const import DOMAIN @@ -134,7 +135,7 @@ async def test_async_entry_reload_on_host_event_received( @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: +async def test_migrate_1_1(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: """Test migration config entry.""" entry_v1_1 = create_config_entry("pchk_v1_1", version=(1, 1)) entry_v1_1.add_to_hass(hass) @@ -143,14 +144,15 @@ async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: await hass.async_block_till_done() entry_migrated = hass.config_entries.async_get_entry(entry_v1_1.entry_id) + assert entry_migrated.state is ConfigEntryState.LOADED assert entry_migrated.version == 2 assert entry_migrated.minor_version == 1 - assert entry_migrated.data == entry.data + assert entry_migrated.data == snapshot @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_migrate_1_2(hass: HomeAssistant, entry) -> None: +async def test_migrate_1_2(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: """Test migration config entry.""" entry_v1_2 = create_config_entry("pchk_v1_2", version=(1, 2)) entry_v1_2.add_to_hass(hass) @@ -159,7 +161,8 @@ async def test_migrate_1_2(hass: HomeAssistant, entry) -> None: await hass.async_block_till_done() entry_migrated = hass.config_entries.async_get_entry(entry_v1_2.entry_id) + assert entry_migrated.state is ConfigEntryState.LOADED assert entry_migrated.version == 2 assert entry_migrated.minor_version == 1 - assert entry_migrated.data == entry.data + assert entry_migrated.data == snapshot diff --git a/tests/components/onedrive/conftest.py b/tests/components/onedrive/conftest.py index 0d6ee09d587..8a0da9f584e 100644 --- a/tests/components/onedrive/conftest.py +++ b/tests/components/onedrive/conftest.py @@ -1,6 +1,7 @@ """Fixtures for OneDrive tests.""" from collections.abc import AsyncIterator, Generator +from json import dumps import time from unittest.mock import AsyncMock, MagicMock, patch @@ -15,11 +16,13 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .const import ( + BACKUP_METADATA, CLIENT_ID, CLIENT_SECRET, MOCK_APPROOT, MOCK_BACKUP_FILE, MOCK_BACKUP_FOLDER, + MOCK_METADATA_FILE, ) from tests.common import MockConfigEntry @@ -89,13 +92,17 @@ def mock_onedrive_client(mock_onedrive_client_init: MagicMock) -> Generator[Magi client = mock_onedrive_client_init.return_value client.get_approot.return_value = MOCK_APPROOT client.create_folder.return_value = MOCK_BACKUP_FOLDER - client.list_drive_items.return_value = [MOCK_BACKUP_FILE] + client.list_drive_items.return_value = [MOCK_BACKUP_FILE, MOCK_METADATA_FILE] client.get_drive_item.return_value = MOCK_BACKUP_FILE + client.upload_file.return_value = MOCK_METADATA_FILE class MockStreamReader: async def iter_chunked(self, chunk_size: int) -> AsyncIterator[bytes]: yield b"backup data" + async def read(self) -> bytes: + return dumps(BACKUP_METADATA).encode() + client.download_drive_item.return_value = MockStreamReader() return client @@ -107,6 +114,7 @@ def mock_large_file_upload_client() -> Generator[AsyncMock]: with patch( "homeassistant.components.onedrive.backup.LargeFileUploadClient.upload" ) as mock_upload: + mock_upload.return_value = MOCK_BACKUP_FILE yield mock_upload diff --git a/tests/components/onedrive/const.py b/tests/components/onedrive/const.py index ee3a5ce3dc4..3739369887d 100644 --- a/tests/components/onedrive/const.py +++ b/tests/components/onedrive/const.py @@ -72,6 +72,29 @@ MOCK_BACKUP_FILE = File( quick_xor_hash="hash", ), mime_type="application/x-tar", - description=escape(dumps(BACKUP_METADATA)), + description="", + created_by=CONTRIBUTOR, +) + +MOCK_METADATA_FILE = File( + id="id", + name="23e64aec.tar", + size=34519040, + parent_reference=ItemParentReference( + drive_id="mock_drive_id", id="id", path="path" + ), + hashes=Hashes( + quick_xor_hash="hash", + ), + mime_type="application/x-tar", + description=escape( + dumps( + { + "metadata_version": 2, + "backup_id": "23e64aec", + "backup_file_id": "id", + } + ) + ), created_by=CONTRIBUTOR, ) diff --git a/tests/components/onedrive/test_backup.py b/tests/components/onedrive/test_backup.py index 0277c3da02e..dd4f4d253d0 100644 --- a/tests/components/onedrive/test_backup.py +++ b/tests/components/onedrive/test_backup.py @@ -152,7 +152,7 @@ async def test_agents_delete( assert response["success"] assert response["result"] == {"agent_errors": {}} - mock_onedrive_client.delete_drive_item.assert_called_once() + assert mock_onedrive_client.delete_drive_item.call_count == 2 async def test_agents_upload( diff --git a/tests/components/onedrive/test_init.py b/tests/components/onedrive/test_init.py index a6ad55442aa..7ceab98ff21 100644 --- a/tests/components/onedrive/test_init.py +++ b/tests/components/onedrive/test_init.py @@ -1,5 +1,7 @@ """Test the OneDrive setup.""" +from html import escape +from json import dumps from unittest.mock import MagicMock from onedrive_personal_sdk.exceptions import AuthenticationError, OneDriveException @@ -9,6 +11,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import setup_integration +from .const import BACKUP_METADATA, MOCK_BACKUP_FILE from tests.common import MockConfigEntry @@ -17,6 +20,7 @@ async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_onedrive_client_init: MagicMock, + mock_onedrive_client: MagicMock, ) -> None: """Test loading and unloading the integration.""" await setup_integration(hass, mock_config_entry) @@ -25,6 +29,10 @@ async def test_load_unload_config_entry( token_callback = mock_onedrive_client_init.call_args[0][0] assert await token_callback() == "mock-access-token" + # make sure metadata migration is not called + assert mock_onedrive_client.upload_file.call_count == 0 + assert mock_onedrive_client.update_drive_item.call_count == 0 + assert mock_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(mock_config_entry.entry_id) @@ -64,3 +72,32 @@ async def test_get_integration_folder_error( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert "Failed to get backups_9f86d081 folder" in caplog.text + + +async def test_migrate_metadata_files( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test migration of metadata files.""" + MOCK_BACKUP_FILE.description = escape( + dumps({**BACKUP_METADATA, "metadata_version": 1}) + ) + await setup_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + mock_onedrive_client.upload_file.assert_called_once() + assert mock_onedrive_client.update_drive_item.call_count == 2 + assert mock_onedrive_client.update_drive_item.call_args[1]["data"].description == "" + + +async def test_migrate_metadata_files_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test migration of metadata files errors.""" + mock_onedrive_client.list_drive_items.side_effect = OneDriveException() + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr index 965d44698c2..811101abe21 100644 --- a/tests/components/shelly/snapshots/test_number.ambr +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -52,7 +52,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15.2', + 'state': 'unknown', }) # --- # name: test_blu_trv_number_entity[number.trv_name_valve_position-entry] diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 15ed098093b..b1b65d99ab5 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -417,24 +417,23 @@ async def test_blu_trv_number_entity( assert entry == snapshot(name=f"{entity_id}-entry") -async def test_blu_trv_set_value( - hass: HomeAssistant, - mock_blu_trv: Mock, - monkeypatch: pytest.MonkeyPatch, +async def test_blu_trv_ext_temp_set_value( + hass: HomeAssistant, mock_blu_trv: Mock ) -> None: - """Test the set value action for BLU TRV number entity.""" + """Test the set value action for BLU TRV External Temperature number entity.""" await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) entity_id = f"{NUMBER_DOMAIN}.trv_name_external_temperature" - assert hass.states.get(entity_id).state == "15.2" + # After HA start the state should be unknown because there was no previous external + # temperature report + assert hass.states.get(entity_id).state is STATE_UNKNOWN - monkeypatch.setitem(mock_blu_trv.status["blutrv:200"], "current_C", 22.2) await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: f"{NUMBER_DOMAIN}.trv_name_external_temperature", + ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 22.2, }, blocking=True, @@ -451,3 +450,44 @@ async def test_blu_trv_set_value( ) assert hass.states.get(entity_id).state == "22.2" + + +async def test_blu_trv_valve_pos_set_value( + hass: HomeAssistant, + mock_blu_trv: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the set value action for BLU TRV Valve Position number entity.""" + # disable automatic temperature control to enable valve position entity + monkeypatch.setitem(mock_blu_trv.config["blutrv:200"], "enable", False) + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + entity_id = f"{NUMBER_DOMAIN}.trv_name_valve_position" + + assert hass.states.get(entity_id).state == "0" + + monkeypatch.setitem(mock_blu_trv.status["blutrv:200"], "pos", 20) + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 20.0, + }, + blocking=True, + ) + mock_blu_trv.mock_update() + mock_blu_trv.call_rpc.assert_called_once_with( + "BluTRV.Call", + { + "id": 200, + "method": "Trv.SetPosition", + "params": {"id": 0, "pos": 20}, + }, + BLU_TRV_TIMEOUT, + ) + # device only accepts int for 'pos' value + assert isinstance(mock_blu_trv.call_rpc.call_args[0][1]["params"]["pos"], int) + + assert hass.states.get(entity_id).state == "20" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 0bbb374012f..ef7771e53ba 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1426,3 +1426,32 @@ async def test_blu_trv_sensor_entity( entry = entity_registry.async_get(entity_id) assert entry == snapshot(name=f"{entity_id}-entry") + + +async def test_rpc_device_virtual_number_sensor_with_device_class( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test a virtual number sensor with device class for RPC device.""" + config = deepcopy(mock_rpc_device.config) + config["number:203"] = { + "name": "Current humidity", + "min": 0, + "max": 100, + "meta": {"ui": {"step": 1, "unit": "%", "view": "label"}}, + "role": "current_humidity", + } + monkeypatch.setattr(mock_rpc_device, "config", config) + + status = deepcopy(mock_rpc_device.status) + status["number:203"] = {"value": 34} + monkeypatch.setattr(mock_rpc_device, "status", status) + + await init_integration(hass, 3) + + state = hass.states.get("sensor.test_name_current_humidity") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 820ab856ebb..679092efe3b 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -553,6 +553,17 @@ async def test_control_error_handling( assert client.play.call_count == int(is_on) +async def test_turn_off_when_device_is_off(hass: HomeAssistant, client) -> None: + """Test no error when turning off device that is already off.""" + await setup_webostv(hass) + client.is_on = False + await client.mock_state_update() + + data = {ATTR_ENTITY_ID: ENTITY_ID} + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data, True) + assert client.power_off.call_count == 1 + + async def test_supported_features(hass: HomeAssistant, client) -> None: """Test test supported features.""" client.sound_output = "lineout" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 573a04e9b57..94566be2f87 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1914,9 +1914,18 @@ async def test_options_flow_migration_reset_old_adapter( assert result4["step_id"] == "choose_serial_port" -async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "device", + [ + "/dev/ttyAMA1", # CM4 + "/dev/ttyAMA10", # CM5, erroneously detected by pyserial + ], +) +async def test_config_flow_port_yellow_port_name( + hass: HomeAssistant, device: str +) -> None: """Test config flow serial port name for Yellow Zigbee radio.""" - port = com_port(device="/dev/ttyAMA1") + port = com_port(device=device) port.serial_number = None port.manufacturer = None port.description = None diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 08b984a0477..be4ace87894 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3378,6 +3378,39 @@ async def test_device_registry_identifiers_collision( assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) +async def test_device_registry_deleted_device_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test update collisions with deleted devices in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE")}, + manufacturer="manufacturer", + model="model", + ) + assert len(device_registry.deleted_devices) == 0 + + device_registry.async_remove_device(device1.id) + assert len(device_registry.deleted_devices) == 1 + + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert len(device_registry.deleted_devices) == 1 + + device_registry.async_update_device( + device2.id, + merge_connections={(dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE")}, + ) + assert len(device_registry.deleted_devices) == 0 + + async def test_primary_config_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 5adfe4fc40b..4317df6cf4a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1176,7 +1176,7 @@ async def test_bootstrap_is_cancellation_safe( @pytest.mark.parametrize("load_registries", [False]) async def test_bootstrap_empty_integrations(hass: HomeAssistant) -> None: """Test setting up an empty integrations does not raise.""" - await bootstrap.async_setup_multi_components(hass, set(), {}) + await bootstrap._async_setup_multi_components(hass, set(), {}) await hass.async_block_till_done() @@ -1311,7 +1311,7 @@ async def test_bootstrap_dependencies( ), ): bootstrap.async_set_domains_to_be_loaded(hass, {integration}) - await bootstrap.async_setup_multi_components(hass, {integration}, {}) + await bootstrap._async_setup_multi_components(hass, {integration}, {}) await hass.async_block_till_done() for assertion in assertions: @@ -1407,7 +1407,7 @@ async def test_cancellation_does_not_leak_upward_from_async_setup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" - await bootstrap.async_setup_multi_components( + await bootstrap._async_setup_multi_components( hass, {"test_package_raises_cancelled_error"}, {} ) await hass.async_block_till_done() @@ -1428,12 +1428,12 @@ async def test_cancellation_does_not_leak_upward_from_async_setup_entry( domain="test_package_raises_cancelled_error_config_entry", data={} ) entry.add_to_hass(hass) - await bootstrap.async_setup_multi_components( + await bootstrap._async_setup_multi_components( hass, {"test_package_raises_cancelled_error_config_entry"}, {} ) await hass.async_block_till_done() - await bootstrap.async_setup_multi_components(hass, {"test_package"}, {}) + await bootstrap._async_setup_multi_components(hass, {"test_package"}, {}) await hass.async_block_till_done() assert ( "Error setting up entry Mock Title for test_package_raises_cancelled_error_config_entry"