diff --git a/.strict-typing b/.strict-typing index cdb0dd8fb96..07f50e009d2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -119,6 +119,7 @@ homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* homeassistant.components.braviatv.* +homeassistant.components.bring.* homeassistant.components.brother.* homeassistant.components.browser.* homeassistant.components.bryant_evolution.* 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/adguard/strings.json b/homeassistant/components/adguard/strings.json index 5b6a5a546f7..44f4a388e6e 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -79,7 +79,7 @@ "services": { "add_url": { "name": "Add URL", - "description": "Add a new filter subscription to AdGuard Home.", + "description": "Adds a new filter subscription to AdGuard Home.", "fields": { "name": { "name": "[%key:common::config_flow::data::name%]", @@ -123,11 +123,11 @@ }, "refresh": { "name": "Refresh", - "description": "Refresh all filter subscriptions in AdGuard Home.", + "description": "Refreshes all filter subscriptions in AdGuard Home.", "fields": { "force": { "name": "Force", - "description": "Force update (bypasses AdGuard Home throttling). \"true\" to force, or \"false\" to omit for a regular refresh." + "description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh." } } } 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/airq/coordinator.py b/homeassistant/components/airq/coordinator.py index 362b65b5828..b48d8047910 100644 --- a/homeassistant/components/airq/coordinator.py +++ b/homeassistant/components/airq/coordinator.py @@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) class AirQCoordinator(DataUpdateCoordinator): """Coordinator is responsible for querying the device at a specified route.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, @@ -33,6 +35,7 @@ class AirQCoordinator(DataUpdateCoordinator): super().__init__( hass, _LOGGER, + config_entry=entry, name=DOMAIN, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) 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/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index 5baa0bcea10..a5a29263140 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -5,12 +5,11 @@ from __future__ import annotations from aioairzone_cloud.cloudapi import AirzoneCloudApi from aioairzone_cloud.common import ConnectionOptions -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -21,8 +20,6 @@ PLATFORMS: list[Platform] = [ Platform.WATER_HEATER, ] -type AirzoneCloudConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] - async def async_setup_entry( hass: HomeAssistant, entry: AirzoneCloudConfigEntry @@ -42,7 +39,7 @@ async def async_setup_entry( airzone.select_installation(inst) await airzone.update_installation(inst) - coordinator = AirzoneUpdateCoordinator(hass, airzone) + coordinator = AirzoneUpdateCoordinator(hass, entry, airzone) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 3d6f6b42901..4a7b5441b68 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -28,8 +28,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, AirzoneEntity, diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index b98473072e4..69b10d2a69e 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -58,8 +58,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, AirzoneEntity, diff --git a/homeassistant/components/airzone_cloud/coordinator.py b/homeassistant/components/airzone_cloud/coordinator.py index e510dcfb401..840bfec0d1b 100644 --- a/homeassistant/components/airzone_cloud/coordinator.py +++ b/homeassistant/components/airzone_cloud/coordinator.py @@ -10,6 +10,7 @@ from typing import Any from aioairzone_cloud.cloudapi import AirzoneCloudApi from aioairzone_cloud.exceptions import AirzoneCloudError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,11 +20,20 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) +type AirzoneCloudConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] + class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Airzone Cloud device.""" - def __init__(self, hass: HomeAssistant, airzone: AirzoneCloudApi) -> None: + config_entry: AirzoneCloudConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: AirzoneCloudConfigEntry, + airzone: AirzoneCloudApi, + ) -> None: """Initialize.""" self.airzone = airzone self.airzone.set_update_callback(self.async_set_updated_data) @@ -31,6 +41,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/airzone_cloud/diagnostics.py b/homeassistant/components/airzone_cloud/diagnostics.py index b6744e36d8c..04aac7e2aa8 100644 --- a/homeassistant/components/airzone_cloud/diagnostics.py +++ b/homeassistant/components/airzone_cloud/diagnostics.py @@ -25,7 +25,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import AirzoneCloudConfigEntry +from .coordinator import AirzoneCloudConfigEntry TO_REDACT_API = [ API_CITY, diff --git a/homeassistant/components/airzone_cloud/select.py b/homeassistant/components/airzone_cloud/select.py index 895796a1073..e0c595a80e8 100644 --- a/homeassistant/components/airzone_cloud/select.py +++ b/homeassistant/components/airzone_cloud/select.py @@ -23,8 +23,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 70d2fd079d4..4b13e09d126 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -49,8 +49,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, AirzoneEntity, diff --git a/homeassistant/components/airzone_cloud/switch.py b/homeassistant/components/airzone_cloud/switch.py index 0eb907ff792..8de0685e15e 100644 --- a/homeassistant/components/airzone_cloud/switch.py +++ b/homeassistant/components/airzone_cloud/switch.py @@ -15,8 +15,7 @@ from homeassistant.components.switch import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity diff --git a/homeassistant/components/airzone_cloud/water_heater.py b/homeassistant/components/airzone_cloud/water_heater.py index 51228ae6b90..381dce913fe 100644 --- a/homeassistant/components/airzone_cloud/water_heater.py +++ b/homeassistant/components/airzone_cloud/water_heater.py @@ -31,8 +31,7 @@ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneCloudConfigEntry -from .coordinator import AirzoneUpdateCoordinator +from .coordinator import AirzoneCloudConfigEntry, AirzoneUpdateCoordinator from .entity import AirzoneHotWaterEntity OPERATION_LIB_TO_HASS: Final[dict[HotWaterOperation, str]] = { diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 04bef105546..8bd393e2d11 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1531,7 +1531,7 @@ async def async_api_adjust_range( data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_delta = directive.payload["rangeValueDelta"] range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) - response_value: int | None = 0 + response_value: float | None = 0 # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": 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/config_flow.py b/homeassistant/components/androidtv/config_flow.py index afaba5175da..d56d4e64b0f 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -387,4 +387,4 @@ def _validate_state_det_rules(state_det_rules: Any) -> list[Any] | None: except ValueError as exc: _LOGGER.warning("Invalid state detection rules: %s", exc) return None - return json_rules # type: ignore[no-any-return] + return json_rules 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/aosmith/__init__.py b/homeassistant/components/aosmith/__init__.py index dd60f69c4b9..7593365c573 100644 --- a/homeassistant/components/aosmith/__init__.py +++ b/homeassistant/components/aosmith/__init__.py @@ -2,31 +2,22 @@ from __future__ import annotations -from dataclasses import dataclass - from py_aosmith import AOSmithAPIClient -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, device_registry as dr from .const import DOMAIN -from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator +from .coordinator import ( + AOSmithConfigEntry, + AOSmithData, + AOSmithEnergyCoordinator, + AOSmithStatusCoordinator, +) PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER] -type AOSmithConfigEntry = ConfigEntry[AOSmithData] - - -@dataclass -class AOSmithData: - """Data for the A. O. Smith integration.""" - - client: AOSmithAPIClient - status_coordinator: AOSmithStatusCoordinator - energy_coordinator: AOSmithEnergyCoordinator - async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> bool: """Set up A. O. Smith from a config entry.""" @@ -36,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> b session = aiohttp_client.async_get_clientsession(hass) client = AOSmithAPIClient(email, password, session) - status_coordinator = AOSmithStatusCoordinator(hass, client) + status_coordinator = AOSmithStatusCoordinator(hass, entry, client) await status_coordinator.async_config_entry_first_refresh() device_registry = dr.async_get(hass) @@ -53,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> b ) energy_coordinator = AOSmithEnergyCoordinator( - hass, client, list(status_coordinator.data) + hass, entry, client, list(status_coordinator.data) ) await energy_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/aosmith/coordinator.py b/homeassistant/components/aosmith/coordinator.py index 3bf97e49cae..26029fee750 100644 --- a/homeassistant/components/aosmith/coordinator.py +++ b/homeassistant/components/aosmith/coordinator.py @@ -1,5 +1,6 @@ """The data update coordinator for the A. O. Smith integration.""" +from dataclasses import dataclass import logging from py_aosmith import ( @@ -9,6 +10,7 @@ from py_aosmith import ( ) from py_aosmith.models import Device as AOSmithDevice +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -17,13 +19,37 @@ from .const import DOMAIN, ENERGY_USAGE_INTERVAL, FAST_INTERVAL, REGULAR_INTERVA _LOGGER = logging.getLogger(__name__) +type AOSmithConfigEntry = ConfigEntry[AOSmithData] + + +@dataclass +class AOSmithData: + """Data for the A. O. Smith integration.""" + + client: AOSmithAPIClient + status_coordinator: "AOSmithStatusCoordinator" + energy_coordinator: "AOSmithEnergyCoordinator" + class AOSmithStatusCoordinator(DataUpdateCoordinator[dict[str, AOSmithDevice]]): """Coordinator for device status, updating with a frequent interval.""" - def __init__(self, hass: HomeAssistant, client: AOSmithAPIClient) -> None: + config_entry: AOSmithConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: AOSmithConfigEntry, + client: AOSmithAPIClient, + ) -> None: """Initialize the coordinator.""" - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=REGULAR_INTERVAL) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=REGULAR_INTERVAL, + ) self.client = client async def _async_update_data(self) -> dict[str, AOSmithDevice]: @@ -51,15 +77,22 @@ class AOSmithStatusCoordinator(DataUpdateCoordinator[dict[str, AOSmithDevice]]): class AOSmithEnergyCoordinator(DataUpdateCoordinator[dict[str, float]]): """Coordinator for energy usage data, updating with a slower interval.""" + config_entry: AOSmithConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: AOSmithConfigEntry, client: AOSmithAPIClient, junction_ids: list[str], ) -> None: """Initialize the coordinator.""" super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=ENERGY_USAGE_INTERVAL + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=ENERGY_USAGE_INTERVAL, ) self.client = client self.junction_ids = junction_ids diff --git a/homeassistant/components/aosmith/diagnostics.py b/homeassistant/components/aosmith/diagnostics.py index 94726731f75..4019bee4dc8 100644 --- a/homeassistant/components/aosmith/diagnostics.py +++ b/homeassistant/components/aosmith/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 AOSmithConfigEntry +from .coordinator import AOSmithConfigEntry TO_REDACT = { "address", diff --git a/homeassistant/components/aosmith/sensor.py b/homeassistant/components/aosmith/sensor.py index b1c9852f647..8a7a98115fa 100644 --- a/homeassistant/components/aosmith/sensor.py +++ b/homeassistant/components/aosmith/sensor.py @@ -15,8 +15,11 @@ from homeassistant.const import PERCENTAGE, UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AOSmithConfigEntry -from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator +from .coordinator import ( + AOSmithConfigEntry, + AOSmithEnergyCoordinator, + AOSmithStatusCoordinator, +) from .entity import AOSmithEnergyEntity, AOSmithStatusEntity diff --git a/homeassistant/components/aosmith/water_heater.py b/homeassistant/components/aosmith/water_heater.py index f3dc8b3413f..110f997065b 100644 --- a/homeassistant/components/aosmith/water_heater.py +++ b/homeassistant/components/aosmith/water_heater.py @@ -17,8 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AOSmithConfigEntry -from .coordinator import AOSmithStatusCoordinator +from .coordinator import AOSmithConfigEntry, AOSmithStatusCoordinator from .entity import AOSmithStatusEntity MODE_HA_TO_AOSMITH = { 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/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index c437f5584db..cdc4563b92d 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -2,16 +2,13 @@ from __future__ import annotations -from dataclasses import dataclass - from APsystemsEZ1 import APsystemsEZ1M -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant from .const import DEFAULT_PORT -from .coordinator import ApSystemsDataCoordinator +from .coordinator import ApSystemsConfigEntry, ApSystemsData, ApSystemsDataCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -21,17 +18,6 @@ PLATFORMS: list[Platform] = [ ] -@dataclass -class ApSystemsData: - """Store runtime data.""" - - coordinator: ApSystemsDataCoordinator - device_id: str - - -type ApSystemsConfigEntry = ConfigEntry[ApSystemsData] - - async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool: """Set up this integration using UI.""" api = APsystemsEZ1M( @@ -40,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> timeout=8, enable_debounce=True, ) - coordinator = ApSystemsDataCoordinator(hass, api) + coordinator = ApSystemsDataCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() assert entry.unique_id entry.runtime_data = ApSystemsData( @@ -51,6 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/apsystems/binary_sensor.py b/homeassistant/components/apsystems/binary_sensor.py index 9e361ca883e..863a50ca455 100644 --- a/homeassistant/components/apsystems/binary_sensor.py +++ b/homeassistant/components/apsystems/binary_sensor.py @@ -17,8 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ApSystemsConfigEntry, ApSystemsData -from .coordinator import ApSystemsDataCoordinator +from .coordinator import ApSystemsConfigEntry, ApSystemsData, ApSystemsDataCoordinator from .entity import ApSystemsEntity diff --git a/homeassistant/components/apsystems/coordinator.py b/homeassistant/components/apsystems/coordinator.py index 2535c66c4ac..ca423055176 100644 --- a/homeassistant/components/apsystems/coordinator.py +++ b/homeassistant/components/apsystems/coordinator.py @@ -12,6 +12,7 @@ from APsystemsEZ1 import ( ReturnOutputData, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,16 +27,34 @@ class ApSystemsSensorData: alarm_info: ReturnAlarmInfo +@dataclass +class ApSystemsData: + """Store runtime data.""" + + coordinator: ApSystemsDataCoordinator + device_id: str + + +type ApSystemsConfigEntry = ConfigEntry[ApSystemsData] + + class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]): """Coordinator used for all sensors.""" + config_entry: ApSystemsConfigEntry device_version: str - def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ApSystemsConfigEntry, + api: APsystemsEZ1M, + ) -> None: """Initialize my coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name="APSystems Data", update_interval=timedelta(seconds=12), ) diff --git a/homeassistant/components/apsystems/entity.py b/homeassistant/components/apsystems/entity.py index 7770b451680..9ba7d046b60 100644 --- a/homeassistant/components/apsystems/entity.py +++ b/homeassistant/components/apsystems/entity.py @@ -5,8 +5,8 @@ from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity -from . import ApSystemsData from .const import DOMAIN +from .coordinator import ApSystemsData class ApSystemsEntity(Entity): diff --git a/homeassistant/components/apsystems/number.py b/homeassistant/components/apsystems/number.py index b5ed60a7754..f7bdc7c2711 100644 --- a/homeassistant/components/apsystems/number.py +++ b/homeassistant/components/apsystems/number.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType -from . import ApSystemsConfigEntry, ApSystemsData +from .coordinator import ApSystemsConfigEntry, ApSystemsData from .entity import ApSystemsEntity diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py index f87bc0f3f26..673dba05acc 100644 --- a/homeassistant/components/apsystems/sensor.py +++ b/homeassistant/components/apsystems/sensor.py @@ -19,8 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ApSystemsConfigEntry, ApSystemsData -from .coordinator import ApSystemsDataCoordinator +from .coordinator import ApSystemsConfigEntry, ApSystemsData, ApSystemsDataCoordinator from .entity import ApSystemsEntity diff --git a/homeassistant/components/apsystems/switch.py b/homeassistant/components/apsystems/switch.py index 73914845445..2d3b0cfd08f 100644 --- a/homeassistant/components/apsystems/switch.py +++ b/homeassistant/components/apsystems/switch.py @@ -11,7 +11,7 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ApSystemsConfigEntry, ApSystemsData +from .coordinator import ApSystemsConfigEntry, ApSystemsData from .entity import ApSystemsEntity 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/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 52d74398818..012b5a19b0f 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AsekoConfigEntry) -> boo except AsekoNotLoggedIn as err: raise ConfigEntryAuthFailed from err - coordinator = AsekoDataUpdateCoordinator(hass, aseko) + coordinator = AsekoDataUpdateCoordinator(hass, entry, aseko) 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/aseko_pool_live/coordinator.py b/homeassistant/components/aseko_pool_live/coordinator.py index 96893912361..d54aa756ddd 100644 --- a/homeassistant/components/aseko_pool_live/coordinator.py +++ b/homeassistant/components/aseko_pool_live/coordinator.py @@ -21,13 +21,18 @@ type AsekoConfigEntry = ConfigEntry[AsekoDataUpdateCoordinator] class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]): """Class to manage fetching Aseko unit data from single endpoint.""" - def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None: + config_entry: AsekoConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: AsekoConfigEntry, aseko: Aseko + ) -> None: """Initialize global Aseko unit data updater.""" self._aseko = aseko super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(minutes=2), ) 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/assist_satellite/intent.py b/homeassistant/components/assist_satellite/intent.py index 75396cf138f..7612753e8c4 100644 --- a/homeassistant/components/assist_satellite/intent.py +++ b/homeassistant/components/assist_satellite/intent.py @@ -1,5 +1,7 @@ """Assist Satellite intents.""" +from typing import Final + import voluptuous as vol from homeassistant.core import HomeAssistant @@ -7,6 +9,8 @@ from homeassistant.helpers import entity_registry as er, intent from .const import DOMAIN, AssistSatelliteEntityFeature +EXCLUDED_DOMAINS: Final[set[str]] = {"voip"} + async def async_setup_intents(hass: HomeAssistant) -> None: """Set up the intents.""" @@ -30,19 +34,36 @@ class BroadcastIntentHandler(intent.IntentHandler): ent_reg = er.async_get(hass) # Find all assist satellite entities that are not the one invoking the intent - entities = { - entity: entry - for entity in hass.states.async_entity_ids(DOMAIN) - if (entry := ent_reg.async_get(entity)) - and entry.supported_features & AssistSatelliteEntityFeature.ANNOUNCE - } + entities: dict[str, er.RegistryEntry] = {} + for entity in hass.states.async_entity_ids(DOMAIN): + entry = ent_reg.async_get(entity) + if ( + (entry is None) + or ( + # Supports announce + not ( + entry.supported_features & AssistSatelliteEntityFeature.ANNOUNCE + ) + ) + # Not the invoking device + or (intent_obj.device_id and (entry.device_id == intent_obj.device_id)) + ): + # Skip satellite + continue - if intent_obj.device_id: - entities = { - entity: entry - for entity, entry in entities.items() - if entry.device_id != intent_obj.device_id - } + # Check domain of config entry against excluded domains + if ( + entry.config_entry_id + and ( + config_entry := hass.config_entries.async_get_entry( + entry.config_entry_id + ) + ) + and (config_entry.domain in EXCLUDED_DOMAINS) + ): + continue + + entities[entity] = entry await hass.services.async_call( DOMAIN, @@ -54,7 +75,6 @@ class BroadcastIntentHandler(intent.IntentHandler): ) response = intent_obj.create_response() - response.async_set_speech("Done") response.response_type = intent.IntentResponseType.ACTION_DONE response.async_set_results( success_results=[ diff --git a/homeassistant/components/atag/coordinator.py b/homeassistant/components/atag/coordinator.py index 6d542471384..f590bc1dc6a 100644 --- a/homeassistant/components/atag/coordinator.py +++ b/homeassistant/components/atag/coordinator.py @@ -19,17 +19,22 @@ type AtagConfigEntry = ConfigEntry[AtagDataUpdateCoordinator] class AtagDataUpdateCoordinator(DataUpdateCoordinator[None]): """Atag data update coordinator.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + config_entry: AtagConfigEntry + + def __init__(self, hass: HomeAssistant, config_entry: AtagConfigEntry) -> None: """Initialize Atag coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Atag", update_interval=timedelta(seconds=60), ) self.atag = AtagOne( - session=async_get_clientsession(hass), **entry.data, device=entry.unique_id + session=async_get_clientsession(hass), + **config_entry.data, + device=config_entry.unique_id, ) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index b6c47cf36b2..a48d704141f 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -1,20 +1,17 @@ """The aurora component.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import CONF_THRESHOLD, DEFAULT_THRESHOLD -from .coordinator import AuroraDataUpdateCoordinator +from .coordinator import AuroraConfigEntry, AuroraDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -type AuroraConfigEntry = ConfigEntry[AuroraDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AuroraConfigEntry) -> bool: """Set up Aurora from a config entry.""" - coordinator = AuroraDataUpdateCoordinator(hass=hass) + coordinator = AuroraDataUpdateCoordinator(hass=hass, config_entry=entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index b8fb5002ff5..648f6de08c9 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AuroraConfigEntry +from .coordinator import AuroraConfigEntry from .entity import AuroraEntity diff --git a/homeassistant/components/aurora/coordinator.py b/homeassistant/components/aurora/coordinator.py index 9771cc53652..a7b87baec22 100644 --- a/homeassistant/components/aurora/coordinator.py +++ b/homeassistant/components/aurora/coordinator.py @@ -4,11 +4,11 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import TYPE_CHECKING from aiohttp import ClientError from auroranoaa import AuroraForecast +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,23 +16,23 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_THRESHOLD, DEFAULT_THRESHOLD -if TYPE_CHECKING: - from . import AuroraConfigEntry - _LOGGER = logging.getLogger(__name__) +type AuroraConfigEntry = ConfigEntry[AuroraDataUpdateCoordinator] + class AuroraDataUpdateCoordinator(DataUpdateCoordinator[int]): """Class to manage fetching data from the NOAA Aurora API.""" config_entry: AuroraConfigEntry - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_entry: AuroraConfigEntry) -> None: """Initialize the data updater.""" super().__init__( hass=hass, logger=_LOGGER, + config_entry=config_entry, name="Aurora", update_interval=timedelta(minutes=5), ) diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 35d39289598..ec1b82c3c4d 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AuroraConfigEntry +from .coordinator import AuroraConfigEntry from .entity import AuroraEntity 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/awair/coordinator.py b/homeassistant/components/awair/coordinator.py index 78f0d9d65f2..62725693522 100644 --- a/homeassistant/components/awair/coordinator.py +++ b/homeassistant/components/awair/coordinator.py @@ -40,17 +40,24 @@ class AwairResult: class AwairDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AwairResult]]): """Define a wrapper class to update Awair data.""" + config_entry: AwairConfigEntry + def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AwairConfigEntry, update_interval: timedelta | None, ) -> None: """Set up the AwairDataUpdateCoordinator class.""" - self._config_entry = config_entry self.title = config_entry.title - 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 _fetch_air_data(self, device: AwairBaseDevice) -> AwairResult: """Fetch latest air quality data.""" @@ -64,7 +71,10 @@ class AwairCloudDataUpdateCoordinator(AwairDataUpdateCoordinator): """Define a wrapper class to update Awair data from Cloud API.""" def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + self, + hass: HomeAssistant, + config_entry: AwairConfigEntry, + session: ClientSession, ) -> None: """Set up the AwairCloudDataUpdateCoordinator class.""" access_token = config_entry.data[CONF_ACCESS_TOKEN] @@ -95,7 +105,10 @@ class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): _device: AwairLocalDevice | None = None def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + self, + hass: HomeAssistant, + config_entry: AwairConfigEntry, + session: ClientSession, ) -> None: """Set up the AwairLocalDataUpdateCoordinator class.""" self._awair = AwairLocal( 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/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 6307d3ca93b..c37fa4615f6 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -411,7 +411,7 @@ def ble_device_matches( ) and service_data_uuid not in service_info.service_data: return False - if manufacturer_id := matcher.get(MANUFACTURER_ID): + if (manufacturer_id := matcher.get(MANUFACTURER_ID)) is not None: if manufacturer_id not in service_info.manufacturer_data: return False 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/__init__.py b/homeassistant/components/bring/__init__.py index 0ee8e3b3155..6dd2d36351c 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -6,19 +6,16 @@ import logging from bring_api import Bring -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import BringDataUpdateCoordinator +from .coordinator import BringConfigEntry, BringDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.TODO] +PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO] _LOGGER = logging.getLogger(__name__) -type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool: """Set up Bring! from a config entry.""" @@ -26,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo session = async_get_clientsession(hass) bring = Bring(session, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD]) - coordinator = BringDataUpdateCoordinator(hass, bring) + coordinator = BringDataUpdateCoordinator(hass, entry, bring) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -36,6 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index bfb5a2cd50f..9e5f4da8356 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -68,7 +68,13 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + description_placeholders={ + "google_play": "https://play.google.com/store/apps/details?id=ch.publisheria.bring", + "app_store": "https://itunes.apple.com/app/apple-store/id580669177", + }, ) async def async_step_reauth( @@ -101,6 +107,29 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconf_entry = self._get_reconfigure_entry() + + if user_input: + if not (errors := await self.validate_input(user_input)): + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort( + reconf_entry, data_updates=user_input + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values={CONF_EMAIL: reconf_entry.data[CONF_EMAIL]}, + ), + errors=errors, + ) + async def validate_input(self, user_input: Mapping[str, Any]) -> dict[str, str]: """Auth Helper.""" diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index 9473d0614e3..e1f9fa45ac8 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -8,11 +8,15 @@ import logging from bring_api import ( Bring, + BringActivityResponse, BringAuthException, + BringItemsResponse, + BringList, BringParseException, BringRequestException, + BringUserSettingsResponse, + BringUsersResponse, ) -from bring_api.types import BringItemsResponse, BringList, BringUserSettingsResponse from mashumaro.mixins.orjson import DataClassORJSONMixin from homeassistant.config_entries import ConfigEntry @@ -26,6 +30,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator] + @dataclass(frozen=True) class BringData(DataClassORJSONMixin): @@ -33,20 +39,25 @@ class BringData(DataClassORJSONMixin): lst: BringList content: BringItemsResponse + activity: BringActivityResponse + users: BringUsersResponse class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): """A Bring Data Update Coordinator.""" - config_entry: ConfigEntry + config_entry: BringConfigEntry user_settings: BringUserSettingsResponse lists: list[BringList] - def __init__(self, hass: HomeAssistant, bring: Bring) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring + ) -> None: """Initialize the Bring data coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=90), ) @@ -59,23 +70,21 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): try: self.lists = (await self.bring.load_lists()).lists except BringRequestException as e: - raise UpdateFailed("Unable to connect and retrieve data from bring") from e + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="setup_request_exception", + ) from e except BringParseException as e: - raise UpdateFailed("Unable to parse response from bring") from e - except BringAuthException: - # try to recover by refreshing access token, otherwise - # initiate reauth flow - try: - await self.bring.retrieve_new_access_token() - except (BringRequestException, BringParseException) as exc: - raise UpdateFailed("Refreshing authentication token failed") from exc - except BringAuthException as exc: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="setup_authentication_exception", - translation_placeholders={CONF_EMAIL: self.bring.mail}, - ) from exc - return self.data + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="setup_parse_exception", + ) from e + except BringAuthException as e: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_EMAIL: self.bring.mail}, + ) from e if self.previous_lists - ( current_lists := {lst.listUuid for lst in self.lists} @@ -89,14 +98,20 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): continue try: items = await self.bring.get_list(lst.listUuid) + activity = await self.bring.get_activity(lst.listUuid) + users = await self.bring.get_list_users(lst.listUuid) except BringRequestException as e: raise UpdateFailed( - "Unable to connect and retrieve data from bring" + translation_domain=DOMAIN, + translation_key="setup_request_exception", ) from e except BringParseException as e: - raise UpdateFailed("Unable to parse response from bring") from e + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="setup_parse_exception", + ) from e else: - list_dict[lst.listUuid] = BringData(lst, items) + list_dict[lst.listUuid] = BringData(lst, items, activity, users) return list_dict diff --git a/homeassistant/components/bring/diagnostics.py b/homeassistant/components/bring/diagnostics.py index 1dec8f3a5ed..6c2f779ef05 100644 --- a/homeassistant/components/bring/diagnostics.py +++ b/homeassistant/components/bring/diagnostics.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.core import HomeAssistant -from . import BringConfigEntry +from .coordinator import BringConfigEntry async def async_get_config_entry_diagnostics( @@ -14,4 +14,8 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - return {k: v.to_dict() for k, v in config_entry.runtime_data.data.items()} + return { + "data": {k: v.to_dict() for k, v in config_entry.runtime_data.data.items()}, + "lists": [lst.to_dict() for lst in config_entry.runtime_data.lists], + "user_settings": config_entry.runtime_data.user_settings.to_dict(), + } diff --git a/homeassistant/components/bring/entity.py b/homeassistant/components/bring/entity.py index 3de0140d82c..ee90f22beef 100644 --- a/homeassistant/components/bring/entity.py +++ b/homeassistant/components/bring/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from bring_api.types import BringList +from bring_api import BringList from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/bring/event.py b/homeassistant/components/bring/event.py new file mode 100644 index 00000000000..699dba9015a --- /dev/null +++ b/homeassistant/components/bring/event.py @@ -0,0 +1,108 @@ +"""Event platform for Bring integration.""" + +from __future__ import annotations + +from dataclasses import asdict +from datetime import datetime + +from bring_api import ActivityType, BringList + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BringConfigEntry +from .coordinator import BringDataUpdateCoordinator +from .entity import BringBaseEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BringConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the event platform.""" + coordinator = config_entry.runtime_data + lists_added: set[str] = set() + + @callback + def add_entities() -> None: + """Add event entities.""" + nonlocal lists_added + + if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added: + async_add_entities( + BringEventEntity( + coordinator, + bring_list, + ) + for bring_list in coordinator.lists + if bring_list.listUuid in new_lists + ) + lists_added |= new_lists + + coordinator.async_add_listener(add_entities) + add_entities() + + +class BringEventEntity(BringBaseEntity, EventEntity): + """An event entity.""" + + _attr_translation_key = "activities" + + def __init__( + self, + coordinator: BringDataUpdateCoordinator, + bring_list: BringList, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator, bring_list) + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}_{self._list_uuid}_activities" + ) + self._attr_event_types = [event.name.lower() for event in ActivityType] + + def _async_handle_event(self) -> None: + """Handle the activity event.""" + bring_list = self.coordinator.data[self._list_uuid] + last_event_triggered = self.state + if bring_list.activity.timeline and ( + last_event_triggered is None + or datetime.fromisoformat(last_event_triggered) + < bring_list.activity.timestamp + ): + activity = bring_list.activity.timeline[0] + attributes = asdict(activity.content) + + attributes["last_activity_by"] = next( + x.name + for x in bring_list.users.users + if x.publicUuid == activity.content.publicUserUuid + ) + + self._trigger_event( + activity.type.name.lower(), + attributes, + ) + self.async_write_ha_state() + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend, if any.""" + + return ( + f"https://api.getbring.com/rest/v2/bringusers/profilepictures/{public_uuid}" + if (public_uuid := self.state_attributes.get("publicUserUuid")) + else super().entity_picture + ) + + async def async_added_to_hass(self) -> None: + """Register callbacks with your device API/library.""" + await super().async_added_to_hass() + self._async_handle_event() + + def _handle_coordinator_update(self) -> None: + self._async_handle_event() + return super()._handle_coordinator_update() diff --git a/homeassistant/components/bring/icons.json b/homeassistant/components/bring/icons.json index c670ef87700..ea4f4e877bc 100644 --- a/homeassistant/components/bring/icons.json +++ b/homeassistant/components/bring/icons.json @@ -1,5 +1,10 @@ { "entity": { + "event": { + "activity": { + "default": "mdi:bell" + } + }, "sensor": { "urgent": { "default": "mdi:run-fast" diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index ecd3e911078..b846cb1c5ca 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["bring_api"], - "requirements": ["bring-api==1.0.0"] + "requirements": ["bring-api==1.0.2"] } diff --git a/homeassistant/components/bring/quality_scale.yaml b/homeassistant/components/bring/quality_scale.yaml index 0b4191d5c61..58e67ab0e11 100644 --- a/homeassistant/components/bring/quality_scale.yaml +++ b/homeassistant/components/bring/quality_scale.yaml @@ -7,7 +7,7 @@ rules: brands: done common-modules: done config-flow-test-coverage: done - config-flow: todo + config-flow: done dependency-transparency: done docs-actions: done docs-high-level-description: todo @@ -58,9 +58,9 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: | @@ -69,4 +69,4 @@ rules: # Platinum async-dependency: done inject-websession: done - strict-typing: todo + strict-typing: done diff --git a/homeassistant/components/bring/sensor.py b/homeassistant/components/bring/sensor.py index 651307a2eee..bfe93619dbb 100644 --- a/homeassistant/components/bring/sensor.py +++ b/homeassistant/components/bring/sensor.py @@ -6,9 +6,8 @@ from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum -from bring_api import BringUserSettingsResponse +from bring_api import BringList, BringUserSettingsResponse from bring_api.const import BRING_SUPPORTED_LOCALES -from bring_api.types import BringList from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,8 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import BringConfigEntry -from .coordinator import BringData, BringDataUpdateCoordinator +from .coordinator import BringConfigEntry, BringData, BringDataUpdateCoordinator from .entity import BringBaseEntity from .util import list_language, sum_attributes diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index ea9af03484e..1dbe0adbf6c 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -5,9 +5,15 @@ "config": { "step": { "user": { + "title": "Bring! Grocery shopping list", + "description": "Connect your Bring! account to sync your shopping lists with Home Assistant.\n\nDon't have a Bring! account? Download the app on [Google Play for Android]({google_play}) or the [App Store for iOS]({app_store}) to sign up.", "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "email": "The email address associated with your Bring! account.", + "password": "The password to login to your Bring! account." } }, "reauth_confirm": { @@ -16,21 +22,53 @@ "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "email": "[%key:component::bring::config::step::user::data_description::email%]", + "password": "[%key:component::bring::config::step::user::data_description::password%]" + } + }, + "reconfigure": { + "title": "Bring! configuration", + "description": "Update your credentials if you have changed your Bring! account email or password.", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "email": "[%key:component::bring::config::step::user::data_description::email%]", + "password": "[%key:component::bring::config::step::user::data_description::password%]" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account." + "unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account.", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { + "event": { + "activities": { + "name": "Activities", + "state_attributes": { + "event_type": { + "state": { + "list_items_added": "Items added", + "list_items_changed": "Items changed", + "list_items_removed": "Items removed" + } + } + } + } + }, "sensor": { "urgent": { "name": "Urgent", diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index ad4de4196c1..4de306273f3 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -9,10 +9,10 @@ import uuid from bring_api import ( BringItem, BringItemOperation, + BringList, BringNotificationType, BringRequestException, ) -from bring_api.types import BringList import voluptuous as vol from homeassistant.components.todo import ( @@ -26,14 +26,13 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BringConfigEntry from .const import ( ATTR_ITEM_NAME, ATTR_NOTIFICATION_TYPE, DOMAIN, SERVICE_PUSH_NOTIFICATION, ) -from .coordinator import BringData, BringDataUpdateCoordinator +from .coordinator import BringConfigEntry, BringData, BringDataUpdateCoordinator from .entity import BringBaseEntity PARALLEL_UPDATES = 0 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/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index c2bf1b2dce1..7a426112d04 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -174,7 +174,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE def __init__( self, - name: str, + name: str | None, entity_id: str, coordinator: CalDavUpdateCoordinator, unique_id: str | None = None, diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index bc6ae29ee8e..adf1e0e981c 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -2,24 +2,21 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started -from .coordinator import CertExpiryDataUpdateCoordinator +from .coordinator import CertExpiryConfigEntry, CertExpiryDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -type CertExpiryConfigEntry = ConfigEntry[CertExpiryDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -> bool: """Load the saved entities.""" host: str = entry.data[CONF_HOST] port: int = entry.data[CONF_PORT] - coordinator = CertExpiryDataUpdateCoordinator(hass, host, port) + coordinator = CertExpiryDataUpdateCoordinator(hass, entry, host, port) entry.runtime_data = coordinator @@ -34,6 +31,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) - return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/cert_expiry/coordinator.py b/homeassistant/components/cert_expiry/coordinator.py index 80c91f1d890..644e3ee3d00 100644 --- a/homeassistant/components/cert_expiry/coordinator.py +++ b/homeassistant/components/cert_expiry/coordinator.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -14,11 +15,21 @@ from .helper import get_cert_expiry_timestamp _LOGGER = logging.getLogger(__name__) +type CertExpiryConfigEntry = ConfigEntry[CertExpiryDataUpdateCoordinator] + class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]): """Class to manage fetching Cert Expiry data from single endpoint.""" - def __init__(self, hass: HomeAssistant, host: str, port: int) -> None: + config_entry: CertExpiryConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: CertExpiryConfigEntry, + host: str, + port: int, + ) -> None: """Initialize global Cert Expiry data updater.""" self.host = host self.port = port @@ -31,6 +42,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=name, update_interval=timedelta(hours=12), always_update=False, diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 4fd0846f0f3..a875e664fdd 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -9,9 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import CertExpiryConfigEntry from .const import DOMAIN -from .coordinator import CertExpiryDataUpdateCoordinator +from .coordinator import CertExpiryConfigEntry, CertExpiryDataUpdateCoordinator from .entity import CertExpiryEntity diff --git a/homeassistant/components/cloud/backup.py b/homeassistant/components/cloud/backup.py index d42e846259c..9531604ccc7 100644 --- a/homeassistant/components/cloud/backup.py +++ b/homeassistant/components/cloud/backup.py @@ -8,16 +8,12 @@ from collections.abc import AsyncIterator, Callable, Coroutine, Mapping import hashlib import logging import random -from typing import Any +from typing import Any, Literal -from aiohttp import ClientError, ClientTimeout +from aiohttp import ClientError from hass_nabucasa import Cloud, CloudError -from hass_nabucasa.cloud_api import ( - async_files_delete_file, - async_files_download_details, - async_files_list, - async_files_upload_details, -) +from hass_nabucasa.api import CloudApiNonRetryableError +from hass_nabucasa.cloud_api import async_files_delete_file, async_files_list from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError from homeassistant.core import HomeAssistant, callback @@ -28,7 +24,7 @@ from .client import CloudClient from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT _LOGGER = logging.getLogger(__name__) -_STORAGE_BACKUP = "backup" +_STORAGE_BACKUP: Literal["backup"] = "backup" _RETRY_LIMIT = 5 _RETRY_SECONDS_MIN = 60 _RETRY_SECONDS_MAX = 600 @@ -109,63 +105,14 @@ class CloudBackupAgent(BackupAgent): raise BackupAgentError("Backup not found") try: - details = await async_files_download_details( - self._cloud, + content = await self._cloud.files.download( storage_type=_STORAGE_BACKUP, filename=self._get_backup_filename(), ) - except (ClientError, CloudError) as err: - raise BackupAgentError("Failed to get download details") from err + except CloudError as err: + raise BackupAgentError(f"Failed to download backup: {err}") from err - try: - resp = await self._cloud.websession.get( - details["url"], - timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h - ) - - resp.raise_for_status() - except ClientError as err: - raise BackupAgentError("Failed to download backup") from err - - return ChunkAsyncStreamIterator(resp.content) - - async def _async_do_upload_backup( - self, - *, - open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], - filename: str, - base64md5hash: str, - metadata: dict[str, Any], - size: int, - ) -> None: - """Upload a backup.""" - try: - details = await async_files_upload_details( - self._cloud, - storage_type=_STORAGE_BACKUP, - filename=filename, - metadata=metadata, - size=size, - base64md5hash=base64md5hash, - ) - except (ClientError, CloudError) as err: - raise BackupAgentError("Failed to get upload details") from err - - try: - upload_status = await self._cloud.websession.put( - details["url"], - data=await open_stream(), - headers=details["headers"] | {"content-length": str(size)}, - timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h - ) - _LOGGER.log( - logging.DEBUG if upload_status.status < 400 else logging.WARNING, - "Backup upload status: %s", - upload_status.status, - ) - upload_status.raise_for_status() - except (TimeoutError, ClientError) as err: - raise BackupAgentError("Failed to upload backup") from err + return ChunkAsyncStreamIterator(content) async def async_upload_backup( self, @@ -190,7 +137,8 @@ class CloudBackupAgent(BackupAgent): tries = 1 while tries <= _RETRY_LIMIT: try: - await self._async_do_upload_backup( + await self._cloud.files.upload( + storage_type=_STORAGE_BACKUP, open_stream=open_stream, filename=filename, base64md5hash=base64md5hash, @@ -198,9 +146,19 @@ class CloudBackupAgent(BackupAgent): size=size, ) break - except BackupAgentError as err: + except CloudApiNonRetryableError as err: + if err.code == "NC-SH-FH-03": + raise BackupAgentError( + translation_domain=DOMAIN, + translation_key="backup_size_too_large", + translation_placeholders={ + "size": str(round(size / (1024**3), 2)) + }, + ) from err + raise BackupAgentError(f"Failed to upload backup {err}") from err + except CloudError as err: if tries == _RETRY_LIMIT: - raise + raise BackupAgentError(f"Failed to upload backup {err}") from err tries += 1 retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX) _LOGGER.info( diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 1da91f67813..6380ee9c312 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -17,6 +17,11 @@ "subscription_expiration": "Subscription expiration" } }, + "exceptions": { + "backup_size_too_large": { + "message": "The backup size of {size}GB is too large to be uploaded to Home Assistant Cloud." + } + }, "issues": { "deprecated_gender": { "title": "The {deprecated_option} text-to-speech option is deprecated", 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/coinbase/const.py b/homeassistant/components/coinbase/const.py index 0f47d4bc208..f20b23dad7a 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -132,6 +132,7 @@ WALLETS = { "GYD": "GYD", "HKD": "HKD", "HNL": "HNL", + "HNT": "HNT", "HRK": "HRK", "HTG": "HTG", "HUF": "HUF", @@ -410,6 +411,7 @@ RATES = { "GYEN": "GYEN", "HKD": "HKD", "HNL": "HNL", + "HNT": "HNT", "HRK": "HRK", "HTG": "HTG", "HUF": "HUF", diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index 96bf021e394..74510731b7a 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -2,19 +2,19 @@ "config": { "step": { "user": { - "title": "Coinbase API Key Details", + "title": "Coinbase API key details", "description": "Please enter the details of your API key as provided by Coinbase.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "api_token": "API Secret" + "api_token": "API secret" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API Key.", - "invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API Secret.", + "invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API key.", + "invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API secret.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { @@ -24,12 +24,12 @@ "options": { "step": { "init": { - "description": "Adjust Coinbase Options", + "description": "Adjust Coinbase options", "data": { "account_balance_currencies": "Wallet balances to report.", "exchange_rate_currencies": "Exchange rates to report.", "exchange_base": "Base currency for exchange rate sensors.", - "exchnage_rate_precision": "Number of decimal places for exchange rates." + "exchange_rate_precision": "Number of decimal places for exchange rates." } } }, 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/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 9b07ae9c875..f9972570df3 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -7,7 +7,6 @@ from ssl import SSLError from deluge_client.client import DelugeRPCClient -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -19,12 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_WEB_PORT -from .coordinator import DelugeDataUpdateCoordinator +from .coordinator import DelugeConfigEntry, DelugeDataUpdateCoordinator PLATFORMS = [Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) -type DelugeConfigEntry = ConfigEntry[DelugeDataUpdateCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: DelugeConfigEntry) -> bool: diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 7f4bf9e884e..c5836243b9d 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -4,10 +4,11 @@ from __future__ import annotations from datetime import timedelta from ssl import SSLError -from typing import TYPE_CHECKING, Any +from typing import Any from deluge_client.client import DelugeRPCClient, FailedToReconnectException +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -15,8 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import LOGGER, DelugeGetSessionStatusKeys -if TYPE_CHECKING: - from . import DelugeConfigEntry +type DelugeConfigEntry = ConfigEntry[DelugeDataUpdateCoordinator] class DelugeDataUpdateCoordinator( @@ -33,11 +33,11 @@ class DelugeDataUpdateCoordinator( super().__init__( hass=hass, logger=LOGGER, + config_entry=entry, name=entry.title, update_interval=timedelta(seconds=30), ) self.api = api - self.config_entry = entry async def _async_update_data(self) -> dict[Platform, dict[str, Any]]: """Get the latest data from Deluge and updates the state.""" diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 5ebf3d01eeb..24d5ce9ec61 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -17,9 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import DelugeConfigEntry from .const import DelugeGetSessionStatusKeys, DelugeSensorType -from .coordinator import DelugeDataUpdateCoordinator +from .coordinator import DelugeConfigEntry, DelugeDataUpdateCoordinator from .entity import DelugeEntity diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index d81f02eee29..1ec0cd7a7df 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -9,8 +9,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DelugeConfigEntry -from .coordinator import DelugeDataUpdateCoordinator +from .coordinator import DelugeConfigEntry, DelugeDataUpdateCoordinator from .entity import DelugeEntity 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/duke_energy/__init__.py b/homeassistant/components/duke_energy/__init__.py index 6eacc15880f..bfa89d81c69 100644 --- a/homeassistant/components/duke_energy/__init__.py +++ b/homeassistant/components/duke_energy/__init__.py @@ -10,7 +10,7 @@ from .coordinator import DukeEnergyConfigEntry, DukeEnergyCoordinator async def async_setup_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool: """Set up Duke Energy from a config entry.""" - coordinator = DukeEnergyCoordinator(hass, entry.data) + coordinator = DukeEnergyCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/duke_energy/coordinator.py b/homeassistant/components/duke_energy/coordinator.py index 2b0ae46b405..12a2f5fd6ae 100644 --- a/homeassistant/components/duke_energy/coordinator.py +++ b/homeassistant/components/duke_energy/coordinator.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta import logging -from types import MappingProxyType from typing import Any, cast from aiodukeenergy import DukeEnergy @@ -37,22 +36,21 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]): config_entry: DukeEnergyConfigEntry def __init__( - self, - hass: HomeAssistant, - entry_data: MappingProxyType[str, Any], + self, hass: HomeAssistant, config_entry: DukeEnergyConfigEntry ) -> None: """Initialize the data handler.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Duke Energy", # Data is updated daily on Duke Energy. # Refresh every 12h to be at most 12h behind. update_interval=timedelta(hours=12), ) self.api = DukeEnergy( - entry_data[CONF_USERNAME], - entry_data[CONF_PASSWORD], + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], async_get_clientsession(hass), ) self._statistic_ids: set = set() 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/eheimdigital/manifest.json b/homeassistant/components/eheimdigital/manifest.json index 7747ca4f95d..1d1ca6f84c7 100644 --- a/homeassistant/components/eheimdigital/manifest.json +++ b/homeassistant/components/eheimdigital/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["eheimdigital"], "quality_scale": "bronze", - "requirements": ["eheimdigital==1.0.5"], + "requirements": ["eheimdigital==1.0.6"], "zeroconf": [ { "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." } ] diff --git a/homeassistant/components/electric_kiwi/manifest.json b/homeassistant/components/electric_kiwi/manifest.json index 9afe487d368..45bb09ca475 100644 --- a/homeassistant/components/electric_kiwi/manifest.json +++ b/homeassistant/components/electric_kiwi/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/electric_kiwi", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["electrickiwi-api==0.9.12"] + "requirements": ["electrickiwi-api==0.9.14"] } 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/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 464d2bcb7e7..9ccb8a64367 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -322,8 +322,10 @@ class HueOneLightStateView(HomeAssistantView): if hass_entity_id is None: _LOGGER.error( - "Unknown entity number: %s not found in emulated_hue_ids.json", + "Unknown entity number: %s not found in emulated_hue_ids.json, " + "state request from %s", entity_id, + request.remote, ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py index da78f3dac5c..16295c7f228 100644 --- a/homeassistant/components/enigma2/__init__.py +++ b/homeassistant/components/enigma2/__init__.py @@ -1,12 +1,9 @@ """Support for Enigma2 devices.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .coordinator import Enigma2UpdateCoordinator - -type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator] +from .coordinator import Enigma2ConfigEntry, Enigma2UpdateCoordinator PLATFORMS = [Platform.MEDIA_PLAYER] @@ -22,6 +19,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/enigma2/coordinator.py b/homeassistant/components/enigma2/coordinator.py index d5bbf2c0ce5..9710d7f547f 100644 --- a/homeassistant/components/enigma2/coordinator.py +++ b/homeassistant/components/enigma2/coordinator.py @@ -30,18 +30,25 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN LOGGER = logging.getLogger(__package__) +type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator] + class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]): """The Enigma2 data update coordinator.""" + config_entry: Enigma2ConfigEntry device: OpenWebIfDevice unique_id: str | None - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, config_entry: Enigma2ConfigEntry) -> None: """Initialize the Enigma2 data update coordinator.""" super().__init__( - hass, logger=LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + hass, + logger=LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, ) base_url = URL.build( diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 1012997ff7f..9a2a4564d1c 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -18,8 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import Enigma2ConfigEntry -from .coordinator import Enigma2UpdateCoordinator +from .coordinator import Enigma2ConfigEntry, Enigma2UpdateCoordinator ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" ATTR_MEDIA_DESCRIPTION = "media_description" diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 8eb2b32ac7b..b8cda03a451 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -55,6 +55,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, _LOGGER, + config_entry=entry, name=entry_data[CONF_NAME], update_interval=SCAN_INTERVAL, always_update=False, diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 97f7c2db54d..e322e266b8a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,22 +2,24 @@ Such systems provide heating/cooling and DHW and include Evohome, Round Thermostat, and others. + +Note that the API used by this integration's client does not support cooling. """ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, Final +from typing import Final -import evohomeasync as ev1 -from evohomeasync.schema import SZ_SESSION_ID -import evohomeasync2 as evo -from evohomeasync2.schema.const import ( - SZ_AUTO_WITH_RESET, - SZ_CAN_BE_TEMPORARY, - SZ_SYSTEM_MODE, - SZ_TIMING_MODE, +import evohomeasync as ec1 +import evohomeasync2 as ec2 +from evohomeasync2.const import SZ_CAN_BE_TEMPORARY, SZ_SYSTEM_MODE, SZ_TIMING_MODE +from evohomeasync2.schemas.const import ( + S2_DURATION as SZ_DURATION, + S2_PERIOD as SZ_PERIOD, + SystemMode as EvoSystemMode, ) import voluptuous as vol @@ -34,14 +36,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import dt as dt_util +from homeassistant.util.hass_dict import HassKey from .const import ( - ACCESS_TOKEN, - ACCESS_TOKEN_EXPIRES, ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, ATTR_DURATION_UNTIL, @@ -49,16 +47,12 @@ from .const import ( ATTR_ZONE_TEMP, CONF_LOCATION_IDX, DOMAIN, - REFRESH_TOKEN, SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_MINIMUM, - STORAGE_KEY, - STORAGE_VER, - USER_DATA, EvoService, ) -from .coordinator import EvoBroker -from .helpers import dt_aware_to_naive, dt_local_to_aware, handle_evo_exception +from .coordinator import EvoDataUpdateCoordinator +from .storage import TokenManager _LOGGER = logging.getLogger(__name__) @@ -96,177 +90,69 @@ SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( } ) +EVOHOME_KEY: HassKey[EvoData] = HassKey(DOMAIN) -class EvoSession: - """Class for evohome client instantiation & authentication.""" - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the evohome broker and its data structure.""" +@dataclass +class EvoData: + """Dataclass for storing evohome data.""" - self.hass = hass - - self._session = async_get_clientsession(hass) - self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) - - # the main client, which uses the newer API - self.client_v2: evo.EvohomeClient | None = None - self._tokens: dict[str, Any] = {} - - # the older client can be used to obtain high-precision temps (only) - self.client_v1: ev1.EvohomeClient | None = None - self.session_id: str | None = None - - async def authenticate(self, username: str, password: str) -> None: - """Check the user credentials against the web API. - - Will raise evo.AuthenticationFailed if the credentials are invalid. - """ - - if ( - self.client_v2 is None - or username != self.client_v2.username - or password != self.client_v2.password - ): - await self._load_auth_tokens(username) - - client_v2 = evo.EvohomeClient( - username, - password, - **self._tokens, - session=self._session, - ) - - else: # force a re-authentication - client_v2 = self.client_v2 - client_v2._user_account = None # noqa: SLF001 - - await client_v2.login() - self.client_v2 = client_v2 # only set attr if authentication succeeded - - await self.save_auth_tokens() - - self.client_v1 = ev1.EvohomeClient( - username, - password, - session_id=self.session_id, - session=self._session, - ) - - async def _load_auth_tokens(self, username: str) -> None: - """Load access tokens and session_id from the store and validate them. - - Sets self._tokens and self._session_id to the latest values. - """ - - app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) - - if app_storage.pop(CONF_USERNAME, None) != username: - # any tokens won't be valid, and store might be corrupt - await self._store.async_save({}) - - self.session_id = None - self._tokens = {} - - return - - # evohomeasync2 requires naive/local datetimes as strings - if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and ( - expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES]) - ): - app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) - - user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) or {} - - self.session_id = user_data.get(SZ_SESSION_ID) - self._tokens = app_storage - - async def save_auth_tokens(self) -> None: - """Save access tokens and session_id to the store. - - Sets self._tokens and self._session_id to the latest values. - """ - - if self.client_v2 is None: - await self._store.async_save({}) - return - - # evohomeasync2 uses naive/local datetimes - access_token_expires = dt_local_to_aware( - self.client_v2.access_token_expires # type: ignore[arg-type] - ) - - self._tokens = { - CONF_USERNAME: self.client_v2.username, - REFRESH_TOKEN: self.client_v2.refresh_token, - ACCESS_TOKEN: self.client_v2.access_token, - ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), - } - - self.session_id = self.client_v1.broker.session_id if self.client_v1 else None - - app_storage = self._tokens - if self.client_v1: - app_storage[USER_DATA] = {SZ_SESSION_ID: self.session_id} - - await self._store.async_save(app_storage) + coordinator: EvoDataUpdateCoordinator + loc_idx: int + tcs: ec2.ControlSystem async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Evohome integration.""" - sess = EvoSession(hass) - - try: - await sess.authenticate( - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - ) - - except (evo.AuthenticationFailed, evo.RequestFailed) as err: - handle_evo_exception(err) - return False - - finally: - config[DOMAIN][CONF_PASSWORD] = "REDACTED" - - broker = EvoBroker(sess) - - if not broker.validate_location( - config[DOMAIN][CONF_LOCATION_IDX], - ): - return False - - coordinator = DataUpdateCoordinator( + token_manager = TokenManager( + hass, + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + async_get_clientsession(hass), + ) + coordinator = EvoDataUpdateCoordinator( hass, _LOGGER, - config_entry=None, + ec2.EvohomeClient(token_manager), name=f"{DOMAIN}_coordinator", update_interval=config[DOMAIN][CONF_SCAN_INTERVAL], - update_method=broker.async_update, + location_idx=config[DOMAIN][CONF_LOCATION_IDX], + client_v1=ec1.EvohomeClient(token_manager), ) + await coordinator.async_register_shutdown() + await coordinator.async_first_refresh() - hass.data[DOMAIN] = {"broker": broker, "coordinator": coordinator} + if not coordinator.last_update_success: + _LOGGER.error(f"Failed to fetch initial data: {coordinator.last_exception}") # noqa: G004 + return False - # without a listener, _schedule_refresh() won't be invoked by _async_refresh() - coordinator.async_add_listener(lambda: None) - await coordinator.async_refresh() # get initial state + assert coordinator.tcs is not None # mypy + + hass.data[EVOHOME_KEY] = EvoData( + coordinator=coordinator, + loc_idx=coordinator.loc_idx, + tcs=coordinator.tcs, + ) hass.async_create_task( async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config) ) - if broker.tcs.hotwater: + if coordinator.tcs.hotwater: hass.async_create_task( async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config) ) - setup_service_functions(hass, broker) + setup_service_functions(hass, coordinator) return True @callback -def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: +def setup_service_functions( + hass: HomeAssistant, coordinator: EvoDataUpdateCoordinator +) -> None: """Set up the service handlers for the system/zone operating modes. Not all Honeywell TCC-compatible systems support all operating modes. In addition, @@ -279,13 +165,15 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: @verify_domain_control(hass, DOMAIN) async def force_refresh(call: ServiceCall) -> None: """Obtain the latest state data via the vendor's RESTful API.""" - await broker.async_update() + await coordinator.async_refresh() @verify_domain_control(hass, DOMAIN) async def set_system_mode(call: ServiceCall) -> None: """Set the system mode.""" + assert coordinator.tcs is not None # mypy + payload = { - "unique_id": broker.tcs.systemId, + "unique_id": coordinator.tcs.id, "service": call.service, "data": call.data, } @@ -313,17 +201,23 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: async_dispatcher_send(hass, DOMAIN, payload) + assert coordinator.tcs is not None # mypy + hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh) # Enumerate which operating modes are supported by this system - modes = broker.tcs.allowedSystemModes + modes = list(coordinator.tcs.allowed_system_modes) # Not all systems support "AutoWithReset": register this handler only if required - if [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]: + if any( + m[SZ_SYSTEM_MODE] + for m in modes + if m[SZ_SYSTEM_MODE] == EvoSystemMode.AUTO_WITH_RESET + ): hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode) system_mode_schemas = [] - modes = [m for m in modes if m[SZ_SYSTEM_MODE] != SZ_AUTO_WITH_RESET] + modes = [m for m in modes if m[SZ_SYSTEM_MODE] != EvoSystemMode.AUTO_WITH_RESET] # Permanent-only modes will use this schema perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]] @@ -334,7 +228,7 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]] # These modes are set for a number of hours (or indefinitely): use this schema - temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == "Duration"] + temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_DURATION] if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours schema = vol.Schema( { @@ -348,7 +242,7 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: system_mode_schemas.append(schema) # These modes are set for a number of days (or indefinitely): use this schema - temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == "Period"] + temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_PERIOD] if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days schema = vol.Schema( { diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 64e7367bc32..8a455b300f8 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -4,20 +4,20 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING, Any +from typing import Any import evohomeasync2 as evo -from evohomeasync2.schema.const import ( - SZ_ACTIVE_FAULTS, +from evohomeasync2.const import ( SZ_SETPOINT_STATUS, - SZ_SYSTEM_ID, SZ_SYSTEM_MODE, SZ_SYSTEM_MODE_STATUS, SZ_TEMPERATURE_STATUS, - SZ_UNTIL, - SZ_ZONE_ID, - ZoneModelType, - ZoneType, +) +from evohomeasync2.schemas.const import ( + SystemMode as EvoSystemMode, + ZoneMode as EvoZoneMode, + ZoneModelType as EvoZoneModelType, + ZoneType as EvoZoneType, ) from homeassistant.components.climate import ( @@ -30,67 +30,46 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.const import PRECISION_TENTHS, UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util +from . import EVOHOME_KEY from .const import ( ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, ATTR_DURATION_UNTIL, ATTR_SYSTEM_MODE, ATTR_ZONE_TEMP, - DOMAIN, - EVO_AUTO, - EVO_AUTOECO, - EVO_AWAY, - EVO_CUSTOM, - EVO_DAYOFF, - EVO_FOLLOW, - EVO_HEATOFF, - EVO_PERMOVER, - EVO_RESET, - EVO_TEMPOVER, EvoService, ) -from .entity import EvoChild, EvoDevice - -if TYPE_CHECKING: - from . import EvoBroker - +from .coordinator import EvoDataUpdateCoordinator +from .entity import EvoChild, EvoEntity _LOGGER = logging.getLogger(__name__) -PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW +PRESET_RESET = "Reset" # reset all child zones to EvoZoneMode.FOLLOW_SCHEDULE PRESET_CUSTOM = "Custom" TCS_PRESET_TO_HA = { - EVO_AWAY: PRESET_AWAY, - EVO_CUSTOM: PRESET_CUSTOM, - EVO_AUTOECO: PRESET_ECO, - EVO_DAYOFF: PRESET_HOME, - EVO_RESET: PRESET_RESET, -} # EVO_AUTO: None, + EvoSystemMode.AWAY: PRESET_AWAY, + EvoSystemMode.CUSTOM: PRESET_CUSTOM, + EvoSystemMode.AUTO_WITH_ECO: PRESET_ECO, + EvoSystemMode.DAY_OFF: PRESET_HOME, + EvoSystemMode.AUTO_WITH_RESET: PRESET_RESET, +} # EvoSystemMode.AUTO: None, HA_PRESET_TO_TCS = {v: k for k, v in TCS_PRESET_TO_HA.items()} EVO_PRESET_TO_HA = { - EVO_FOLLOW: PRESET_NONE, - EVO_TEMPOVER: "temporary", - EVO_PERMOVER: "permanent", + EvoZoneMode.FOLLOW_SCHEDULE: PRESET_NONE, + EvoZoneMode.TEMPORARY_OVERRIDE: "temporary", + EvoZoneMode.PERMANENT_OVERRIDE: "permanent", } HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} -STATE_ATTRS_TCS = [SZ_SYSTEM_ID, SZ_ACTIVE_FAULTS, SZ_SYSTEM_MODE_STATUS] -STATE_ATTRS_ZONES = [ - SZ_ZONE_ID, - SZ_ACTIVE_FAULTS, - SZ_SETPOINT_STATUS, - SZ_TEMPERATURE_STATUS, -] - async def async_setup_platform( hass: HomeAssistant, @@ -102,32 +81,34 @@ async def async_setup_platform( if discovery_info is None: return - broker: EvoBroker = hass.data[DOMAIN]["broker"] + coordinator = hass.data[EVOHOME_KEY].coordinator + loc_idx = hass.data[EVOHOME_KEY].loc_idx + tcs = hass.data[EVOHOME_KEY].tcs _LOGGER.debug( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", - broker.tcs.modelType, - broker.tcs.systemId, - broker.loc.name, - broker.loc_idx, + tcs.model, + tcs.id, + tcs.location.name, + loc_idx, ) - entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] + entities: list[EvoController | EvoZone] = [EvoController(coordinator, tcs)] - for zone in broker.tcs.zones.values(): + for zone in tcs.zones: if ( - zone.modelType == ZoneModelType.HEATING_ZONE - or zone.zoneType == ZoneType.THERMOSTAT + zone.model == EvoZoneModelType.HEATING_ZONE + or zone.type == EvoZoneType.THERMOSTAT ): _LOGGER.debug( "Adding: %s (%s), id=%s, name=%s", - zone.zoneType, - zone.modelType, - zone.zoneId, + zone.type, + zone.model, + zone.id, zone.name, ) - new_entity = EvoZone(broker, zone) + new_entity = EvoZone(coordinator, zone) entities.append(new_entity) else: @@ -136,16 +117,19 @@ async def async_setup_platform( "Ignoring: %s (%s), id=%s, name=%s: unknown/invalid zone type, " "report as an issue if you feel this zone type should be supported" ), - zone.zoneType, - zone.modelType, - zone.zoneId, + zone.type, + zone.model, + zone.id, zone.name, ) - async_add_entities(entities, update_before_add=True) + async_add_entities(entities) + + for entity in entities: + await entity.update_attrs() -class EvoClimateEntity(EvoDevice, ClimateEntity): +class EvoClimateEntity(EvoEntity, ClimateEntity): """Base for any evohome-compatible climate entity (controller, zone).""" _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] @@ -157,25 +141,29 @@ class EvoZone(EvoChild, EvoClimateEntity): _attr_preset_modes = list(HA_PRESET_TO_EVO) - _evo_device: evo.Zone # mypy hint + _evo_device: evo.Zone + _evo_id_attr = "zone_id" + _evo_state_attr_names = (SZ_SETPOINT_STATUS, SZ_TEMPERATURE_STATUS) - def __init__(self, evo_broker: EvoBroker, evo_device: evo.Zone) -> None: + def __init__( + self, coordinator: EvoDataUpdateCoordinator, evo_device: evo.Zone + ) -> None: """Initialize an evohome-compatible heating zone.""" - super().__init__(evo_broker, evo_device) - self._evo_id = evo_device.zoneId + super().__init__(coordinator, evo_device) + self._evo_id = evo_device.id - if evo_device.modelType.startswith("VisionProWifi"): + if evo_device.model.startswith("VisionProWifi"): # this system does not have a distinct ID for the zone - self._attr_unique_id = f"{evo_device.zoneId}z" + self._attr_unique_id = f"{evo_device.id}z" else: - self._attr_unique_id = evo_device.zoneId + self._attr_unique_id = evo_device.id - if evo_broker.client_v1: + if coordinator.client_v1: self._attr_precision = PRECISION_TENTHS else: - self._attr_precision = self._evo_device.setpointCapabilities[ - "valueResolution" + self._attr_precision = self._evo_device.setpoint_capabilities[ + "value_resolution" ] self._attr_supported_features = ( @@ -188,7 +176,7 @@ class EvoZone(EvoChild, EvoClimateEntity): async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" if service == EvoService.RESET_ZONE_OVERRIDE: - await self._evo_broker.call_client_api(self._evo_device.reset_mode()) + await self.coordinator.call_client_api(self._evo_device.reset()) return # otherwise it is EvoService.SET_ZONE_OVERRIDE @@ -198,14 +186,14 @@ class EvoZone(EvoChild, EvoClimateEntity): duration: timedelta = data[ATTR_DURATION_UNTIL] if duration.total_seconds() == 0: await self._update_schedule() - until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) + until = self.setpoints.get("next_sp_from") else: until = dt_util.now() + data[ATTR_DURATION_UNTIL] else: until = None # indefinitely until = dt_util.as_utc(until) if until else None - await self._evo_broker.call_client_api( + await self.coordinator.call_client_api( self._evo_device.set_temperature(temperature, until=until) ) @@ -217,7 +205,7 @@ class EvoZone(EvoChild, EvoClimateEntity): @property def hvac_mode(self) -> HVACMode | None: """Return the current operating mode of a Zone.""" - if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF): + if self._evo_tcs.mode in (EvoSystemMode.AWAY, EvoSystemMode.HEATING_OFF): return HVACMode.AUTO if self.target_temperature is None: return None @@ -233,10 +221,8 @@ class EvoZone(EvoChild, EvoClimateEntity): @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" - if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF): - return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode) - if self._evo_device.mode is None: - return None + if self._evo_tcs.mode in (EvoSystemMode.AWAY, EvoSystemMode.HEATING_OFF): + return TCS_PRESET_TO_HA.get(self._evo_tcs.mode) return EVO_PRESET_TO_HA.get(self._evo_device.mode) @property @@ -245,8 +231,6 @@ class EvoZone(EvoChild, EvoClimateEntity): The default is 5, but is user-configurable within 5-21 (in Celsius). """ - if self._evo_device.min_heat_setpoint is None: - return 5 return self._evo_device.min_heat_setpoint @property @@ -255,33 +239,27 @@ class EvoZone(EvoChild, EvoClimateEntity): The default is 35, but is user-configurable within 21-35 (in Celsius). """ - if self._evo_device.max_heat_setpoint is None: - return 35 return self._evo_device.max_heat_setpoint async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" - assert self._evo_device.setpointStatus is not None # mypy check - temperature = kwargs["temperature"] if (until := kwargs.get("until")) is None: - if self._evo_device.mode == EVO_FOLLOW: + if self._evo_device.mode == EvoZoneMode.TEMPORARY_OVERRIDE: + until = self._evo_device.until + if self._evo_device.mode == EvoZoneMode.FOLLOW_SCHEDULE: await self._update_schedule() - until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) - elif self._evo_device.mode == EVO_TEMPOVER: - until = dt_util.parse_datetime( - self._evo_device.setpointStatus[SZ_UNTIL] - ) + until = self.setpoints.get("next_sp_from") until = dt_util.as_utc(until) if until else None - await self._evo_broker.call_client_api( + await self.coordinator.call_client_api( self._evo_device.set_temperature(temperature, until=until) ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set a Zone to one of its native EVO_* operating modes. + """Set a Zone to one of its native operating modes. Zones inherit their _effective_ operating mode from their Controller. @@ -298,41 +276,34 @@ class EvoZone(EvoChild, EvoClimateEntity): and 'Away', Zones to (by default) 12C. """ if hvac_mode == HVACMode.OFF: - await self._evo_broker.call_client_api( + await self.coordinator.call_client_api( self._evo_device.set_temperature(self.min_temp, until=None) ) else: # HVACMode.HEAT - await self._evo_broker.call_client_api(self._evo_device.reset_mode()) + await self.coordinator.call_client_api(self._evo_device.reset()) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to following the schedule.""" - evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) + evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EvoZoneMode.FOLLOW_SCHEDULE) - if evo_preset_mode == EVO_FOLLOW: - await self._evo_broker.call_client_api(self._evo_device.reset_mode()) + if evo_preset_mode == EvoZoneMode.FOLLOW_SCHEDULE: + await self.coordinator.call_client_api(self._evo_device.reset()) return - if evo_preset_mode == EVO_TEMPOVER: + if evo_preset_mode == EvoZoneMode.TEMPORARY_OVERRIDE: await self._update_schedule() - until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) - else: # EVO_PERMOVER + until = self.setpoints.get("next_sp_from") + else: # EvoZoneMode.PERMANENT_OVERRIDE until = None temperature = self._evo_device.target_heat_temperature assert temperature is not None # mypy check until = dt_util.as_utc(until) if until else None - await self._evo_broker.call_client_api( + await self.coordinator.call_client_api( self._evo_device.set_temperature(temperature, until=until) ) - async def async_update(self) -> None: - """Get the latest state data for a Zone.""" - await super().async_update() - - for attr in STATE_ATTRS_ZONES: - self._device_state_attrs[attr] = getattr(self._evo_device, attr) - class EvoController(EvoClimateEntity): """Base for any evohome-compatible controller. @@ -347,18 +318,22 @@ class EvoController(EvoClimateEntity): _attr_icon = "mdi:thermostat" _attr_precision = PRECISION_TENTHS - _evo_device: evo.ControlSystem # mypy hint + _evo_device: evo.ControlSystem + _evo_id_attr = "system_id" + _evo_state_attr_names = (SZ_SYSTEM_MODE_STATUS,) - def __init__(self, evo_broker: EvoBroker, evo_device: evo.ControlSystem) -> None: + def __init__( + self, coordinator: EvoDataUpdateCoordinator, evo_device: evo.ControlSystem + ) -> None: """Initialize an evohome-compatible controller.""" - super().__init__(evo_broker, evo_device) - self._evo_id = evo_device.systemId + super().__init__(coordinator, evo_device) + self._evo_id = evo_device.id - self._attr_unique_id = evo_device.systemId + self._attr_unique_id = evo_device.id self._attr_name = evo_device.location.name - self._evo_modes = [m[SZ_SYSTEM_MODE] for m in evo_device.allowedSystemModes] + self._evo_modes = [m[SZ_SYSTEM_MODE] for m in evo_device.allowed_system_modes] self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in self._evo_modes if m in list(TCS_PRESET_TO_HA) ] @@ -376,7 +351,7 @@ class EvoController(EvoClimateEntity): if service == EvoService.SET_SYSTEM_MODE: mode = data[ATTR_SYSTEM_MODE] else: # otherwise it is EvoService.RESET_SYSTEM - mode = EVO_RESET + mode = EvoSystemMode.AUTO_WITH_RESET if ATTR_DURATION_DAYS in data: until = dt_util.start_of_local_day() @@ -390,18 +365,24 @@ class EvoController(EvoClimateEntity): await self._set_tcs_mode(mode, until=until) - async def _set_tcs_mode(self, mode: str, until: datetime | None = None) -> None: - """Set a Controller to any of its native EVO_* operating modes.""" + async def _set_tcs_mode( + self, mode: EvoSystemMode, until: datetime | None = None + ) -> None: + """Set a Controller to any of its native operating modes.""" until = dt_util.as_utc(until) if until else None - await self._evo_broker.call_client_api( - self._evo_device.set_mode(mode, until=until) # type: ignore[arg-type] + await self.coordinator.call_client_api( + self._evo_device.set_mode(mode, until=until) ) @property def hvac_mode(self) -> HVACMode: """Return the current operating mode of a Controller.""" - evo_mode = self._evo_device.system_mode - return HVACMode.OFF if evo_mode in (EVO_HEATOFF, "Off") else HVACMode.HEAT + evo_mode = self._evo_device.mode + return ( + HVACMode.OFF + if evo_mode in (EvoSystemMode.HEATING_OFF, EvoSystemMode.OFF) + else HVACMode.HEAT + ) @property def current_temperature(self) -> float | None: @@ -410,18 +391,14 @@ class EvoController(EvoClimateEntity): Controllers do not have a current temp, but one is expected by HA. """ temps = [ - z.temperature - for z in self._evo_device.zones.values() - if z.temperature is not None + z.temperature for z in self._evo_device.zones if z.temperature is not None ] return round(sum(temps) / len(temps), 1) if temps else None @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" - if not self._evo_device.system_mode: - return None - return TCS_PRESET_TO_HA.get(self._evo_device.system_mode) + return TCS_PRESET_TO_HA.get(self._evo_device.mode) async def async_set_temperature(self, **kwargs: Any) -> None: """Raise exception as Controllers don't have a target temperature.""" @@ -429,25 +406,40 @@ class EvoController(EvoClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set an operating mode for a Controller.""" + + evo_mode: EvoSystemMode + if hvac_mode == HVACMode.HEAT: - evo_mode = EVO_AUTO if EVO_AUTO in self._evo_modes else "Heat" + evo_mode = ( + EvoSystemMode.AUTO + if EvoSystemMode.AUTO in self._evo_modes + else EvoSystemMode.HEAT + ) elif hvac_mode == HVACMode.OFF: - evo_mode = EVO_HEATOFF if EVO_HEATOFF in self._evo_modes else "Off" + evo_mode = ( + EvoSystemMode.HEATING_OFF + if EvoSystemMode.HEATING_OFF in self._evo_modes + else EvoSystemMode.OFF + ) else: raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}") await self._set_tcs_mode(evo_mode) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to 'Auto' mode.""" - await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) + await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EvoSystemMode.AUTO)) - async def async_update(self) -> None: - """Get the latest state data for a Controller.""" - self._device_state_attrs = {} + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" - attrs = self._device_state_attrs - for attr in STATE_ATTRS_TCS: - if attr == SZ_ACTIVE_FAULTS: - attrs["activeSystemFaults"] = getattr(self._evo_device, attr) - else: - attrs[attr] = getattr(self._evo_device, attr) + self._device_state_attrs = { + "activeSystemFaults": self._evo_device.active_faults + + self._evo_device.gateway.active_faults + } + + super()._handle_coordinator_update() + + async def update_attrs(self) -> None: + """Update the entity's extra state attrs.""" + self._handle_coordinator_update() diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index 3ebe6954fea..12642addfa4 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -11,31 +11,8 @@ DOMAIN: Final = "evohome" STORAGE_VER: Final = 1 STORAGE_KEY: Final = DOMAIN -# The Parent's (i.e. TCS, Controller) operating mode is one of: -EVO_RESET: Final = "AutoWithReset" -EVO_AUTO: Final = "Auto" -EVO_AUTOECO: Final = "AutoWithEco" -EVO_AWAY: Final = "Away" -EVO_DAYOFF: Final = "DayOff" -EVO_CUSTOM: Final = "Custom" -EVO_HEATOFF: Final = "HeatingOff" - -# The Children's (i.e. Dhw, Zone) operating mode is one of: -EVO_FOLLOW: Final = "FollowSchedule" # the operating mode is 'inherited' from the TCS -EVO_TEMPOVER: Final = "TemporaryOverride" -EVO_PERMOVER: Final = "PermanentOverride" - -# These two are used only to help prevent E501 (line too long) violations -GWS: Final = "gateways" -TCS: Final = "temperatureControlSystems" - -UTC_OFFSET: Final = "currentOffsetMinutes" - CONF_LOCATION_IDX: Final = "location_idx" -ACCESS_TOKEN: Final = "access_token" -ACCESS_TOKEN_EXPIRES: Final = "access_token_expires" -REFRESH_TOKEN: Final = "refresh_token" USER_DATA: Final = "user_data" SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 943bd6605b4..7b197f1b643 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -4,109 +4,143 @@ from __future__ import annotations from collections.abc import Awaitable from datetime import timedelta +from http import HTTPStatus import logging -from typing import TYPE_CHECKING, Any +from typing import Any -import evohomeasync as ev1 -from evohomeasync.schema import SZ_ID, SZ_TEMP -import evohomeasync2 as evo -from evohomeasync2.schema.const import ( +import evohomeasync as ec1 +import evohomeasync2 as ec2 +from evohomeasync2.const import ( SZ_GATEWAY_ID, SZ_GATEWAY_INFO, + SZ_GATEWAYS, SZ_LOCATION_ID, SZ_LOCATION_INFO, + SZ_TEMPERATURE_CONTROL_SYSTEMS, SZ_TIME_ZONE, + SZ_USE_DAYLIGHT_SAVE_SWITCHING, ) +from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET -from .helpers import handle_evo_exception - -if TYPE_CHECKING: - from . import EvoSession - -_LOGGER = logging.getLogger(__name__.rpartition(".")[0]) +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -class EvoBroker: - """Broker for evohome client broker.""" +class EvoDataUpdateCoordinator(DataUpdateCoordinator): + """Coordinator for evohome integration/client.""" - loc_idx: int - loc: evo.Location - loc_utc_offset: timedelta - tcs: evo.ControlSystem + # These will not be None after _async_setup()) + loc: ec2.Location + tcs: ec2.ControlSystem - def __init__(self, sess: EvoSession) -> None: - """Initialize the evohome broker and its data structure.""" + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + client_v2: ec2.EvohomeClient, + *, + name: str, + update_interval: timedelta, + location_idx: int, + client_v1: ec1.EvohomeClient | None = None, + ) -> None: + """Class to manage fetching data.""" - self._sess = sess - self.hass = sess.hass + super().__init__( + hass, + logger, + config_entry=None, + name=name, + update_interval=update_interval, + ) - assert sess.client_v2 is not None # mypy + self.client = client_v2 + self.client_v1 = client_v1 - self.client = sess.client_v2 - self.client_v1 = sess.client_v1 + self.loc_idx = location_idx + self.data: EvoLocStatusResponseT = None # type: ignore[assignment] self.temps: dict[str, float | None] = {} - def validate_location(self, loc_idx: int) -> bool: - """Get the default TCS of the specified location.""" + self._first_refresh_done = False # get schedules only after first refresh - self.loc_idx = loc_idx + # our version of async_config_entry_first_refresh()... + async def async_first_refresh(self) -> None: + """Refresh data for the first time when integration is setup. - assert self.client.installation_info is not None # mypy + This integration does not have config flow, so it is inappropriate to + invoke `async_config_entry_first_refresh()`. + """ + + # can't replicate `if not await self.__wrap_async_setup():` (is mangled), so... + if not await self._DataUpdateCoordinator__wrap_async_setup(): # type: ignore[attr-defined] + return + + await self._async_refresh( + log_failures=False, raise_on_auth_failed=True, raise_on_entry_error=True + ) + + async def _async_setup(self) -> None: + """Set up the coordinator. + + Fetch the user information, and the configuration of their locations. + """ try: - loc_config = self.client.installation_info[loc_idx] - except IndexError: - _LOGGER.error( - ( - "Config error: '%s' = %s, but the valid range is 0-%s. " - "Unable to continue. Fix any configuration errors and restart HA" - ), - CONF_LOCATION_IDX, - loc_idx, - len(self.client.installation_info) - 1, - ) - return False + await self.client.update(dont_update_status=True) # only config for now + except ec2.EvohomeError as err: + raise UpdateFailed(err) from err - self.loc = self.client.locations[loc_idx] - self.loc_utc_offset = timedelta(minutes=self.loc.timeZone[UTC_OFFSET]) - self.tcs = self.loc._gateways[0]._control_systems[0] # noqa: SLF001 + try: + self.loc = self.client.locations[self.loc_idx] + except IndexError as err: + raise UpdateFailed( + f""" + Config error: 'location_idx' = {self.loc_idx}, + but the valid range is 0-{len(self.client.locations) - 1}. + Unable to continue. Fix any configuration errors and restart HA + """ + ) from err - if _LOGGER.isEnabledFor(logging.DEBUG): + self.tcs = self.loc.gateways[0].systems[0] + + if self.logger.isEnabledFor(logging.DEBUG): loc_info = { - SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], - SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + SZ_LOCATION_ID: self.loc.id, + SZ_TIME_ZONE: self.loc.config[SZ_TIME_ZONE], + SZ_USE_DAYLIGHT_SAVE_SWITCHING: self.loc.config[ + SZ_USE_DAYLIGHT_SAVE_SWITCHING + ], } gwy_info = { - SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], - TCS: loc_config[GWS][0][TCS], + SZ_GATEWAY_ID: self.loc.gateways[0].id, + SZ_TEMPERATURE_CONTROL_SYSTEMS: [ + self.loc.gateways[0].systems[0].config + ], } config = { SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info}], + SZ_GATEWAYS: [{SZ_GATEWAY_INFO: gwy_info}], } - _LOGGER.debug("Config = %s", config) - - return True + self.logger.debug("Config = %s", config) async def call_client_api( self, client_api: Awaitable[dict[str, Any] | None], - update_state: bool = True, + request_refresh: bool = True, ) -> dict[str, Any] | None: - """Call a client API and update the broker state if required.""" + """Call a client API and update the Coordinator state if required.""" try: result = await client_api - except evo.RequestFailed as err: - handle_evo_exception(err) + + except ec2.ApiRequestFailedError as err: + self.logger.error(err) return None - if update_state: # wait a moment for system to quiesce before updating state - await self.hass.data[DOMAIN]["coordinator"].async_request_refresh() + if request_refresh: # wait a moment for system to quiesce before updating state + await self.async_request_refresh() # hass.async_create_task() won't help return result @@ -115,80 +149,82 @@ class EvoBroker: assert self.client_v1 is not None # mypy check - old_session_id = self._sess.session_id - try: - temps = await self.client_v1.get_temperatures() + await self.client_v1.update() - except ev1.InvalidSchema as err: - _LOGGER.warning( + except ec1.BadUserCredentialsError as err: + self.logger.warning( ( "Unable to obtain high-precision temperatures. " - "It appears the JSON schema is not as expected, " - "so the high-precision feature will be disabled until next restart." - "Message is: %s" + "The feature will be disabled until next restart: %r" ), err, ) self.client_v1 = None - except ev1.RequestFailed as err: - _LOGGER.warning( + except ec1.EvohomeError as err: + self.logger.warning( ( "Unable to obtain the latest high-precision temperatures. " - "Check your network and the vendor's service status page. " - "Proceeding without high-precision temperatures for now. " - "Message is: %s" + "They will be ignored this refresh cycle: %r" ), err, ) self.temps = {} # high-precision temps now considered stale - except Exception: - self.temps = {} # high-precision temps now considered stale - raise - else: - if str(self.client_v1.location_id) != self.loc.locationId: - _LOGGER.warning( - "The v2 API's configured location doesn't match " - "the v1 API's default location (there is more than one location), " - "so the high-precision feature will be disabled until next restart" - ) - self.client_v1 = None - else: - self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} + self.temps = await self.client_v1.location_by_id[ + self.loc.id + ].get_temperatures(dont_update_status=True) - finally: - if self.client_v1 and self.client_v1.broker.session_id != old_session_id: - await self._sess.save_auth_tokens() - - _LOGGER.debug("Temperatures = %s", self.temps) + self.logger.debug("Status (high-res temps) = %s", self.temps) async def _update_v2_api_state(self, *args: Any) -> None: """Get the latest modes, temperatures, setpoints of a Location.""" - access_token = self.client.access_token # maybe receive a new token? - try: - status = await self.loc.refresh_status() - except evo.RequestFailed as err: - handle_evo_exception(err) - else: - async_dispatcher_send(self.hass, DOMAIN) - _LOGGER.debug("Status = %s", status) - finally: - if access_token != self.client.access_token: - await self._sess.save_auth_tokens() + status = await self.loc.update() - async def async_update(self, *args: Any) -> None: - """Get the latest state data of an entire Honeywell TCC Location. + except ec2.ApiRequestFailedError as err: + if err.status != HTTPStatus.TOO_MANY_REQUESTS: + raise UpdateFailed(err) from err + + raise UpdateFailed( + f""" + The vendor's API rate limit has been exceeded. + Consider increasing the {CONF_SCAN_INTERVAL} + """ + ) from err + + except ec2.EvohomeError as err: + raise UpdateFailed(err) from err + + self.logger.debug("Status = %s", status) + + async def _update_v2_schedules(self) -> None: + for zone in self.tcs.zones: + await zone.get_schedule() + + if dhw := self.tcs.hotwater: + await dhw.get_schedule() + + async def _async_update_data(self) -> EvoLocStatusResponseT: # type: ignore[override] + """Fetch the latest state of an entire TCC Location. This includes state data for a Controller and all its child devices, such as the operating mode of the Controller and the current temp of its children (e.g. Zones, DHW controller). """ - await self._update_v2_api_state() + await self._update_v2_api_state() # may raise UpdateFailed if self.client_v1: - await self._update_v1_api_temps() + await self._update_v1_api_temps() # will never raise UpdateFailed + + # to speed up HA startup, don't update entity schedules during initial + # async_first_refresh(), only during subsequent async_refresh()... + if self._first_refresh_done: + await self._update_v2_schedules() + else: + self._first_refresh_done = True + + return self.loc.status diff --git a/homeassistant/components/evohome/entity.py b/homeassistant/components/evohome/entity.py index a42d8ef7582..11215dd47b6 100644 --- a/homeassistant/components/evohome/entity.py +++ b/homeassistant/components/evohome/entity.py @@ -1,55 +1,49 @@ """Base for evohome entity.""" -from datetime import datetime, timedelta, timezone +from collections.abc import Mapping +from datetime import UTC, datetime import logging from typing import Any import evohomeasync2 as evo -from evohomeasync2.schema.const import ( - SZ_HEAT_SETPOINT, - SZ_SETPOINT_STATUS, - SZ_STATE_STATUS, - SZ_SYSTEM_MODE_STATUS, - SZ_TIME_UNTIL, - SZ_UNTIL, -) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity -from homeassistant.util import dt as dt_util +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import EvoBroker, EvoService -from .const import DOMAIN -from .helpers import convert_dict, convert_until +from .const import DOMAIN, EvoService +from .coordinator import EvoDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class EvoDevice(Entity): +class EvoEntity(CoordinatorEntity[EvoDataUpdateCoordinator]): """Base for any evohome-compatible entity (controller, DHW, zone). This includes the controller, (1 to 12) heating zones and (optionally) a DHW controller. """ - _attr_should_poll = False + _evo_device: evo.ControlSystem | evo.HotWater | evo.Zone + _evo_id_attr: str + _evo_state_attr_names: tuple[str, ...] def __init__( self, - evo_broker: EvoBroker, + coordinator: EvoDataUpdateCoordinator, evo_device: evo.ControlSystem | evo.HotWater | evo.Zone, ) -> None: """Initialize an evohome-compatible entity (TCS, DHW, zone).""" + super().__init__(coordinator, context=evo_device.id) self._evo_device = evo_device - self._evo_broker = evo_broker self._device_state_attrs: dict[str, Any] = {} - async def async_refresh(self, payload: dict | None = None) -> None: + async def process_signal(self, payload: dict | None = None) -> None: """Process any signals.""" + if payload is None: - self.async_schedule_update_ha_state(force_refresh=True) - return + raise NotImplementedError if payload["unique_id"] != self._attr_unique_id: return if payload["service"] in ( @@ -69,40 +63,46 @@ class EvoDevice(Entity): raise NotImplementedError @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the evohome-specific state attributes.""" - status = self._device_state_attrs - if SZ_SYSTEM_MODE_STATUS in status: - convert_until(status[SZ_SYSTEM_MODE_STATUS], SZ_TIME_UNTIL) - if SZ_SETPOINT_STATUS in status: - convert_until(status[SZ_SETPOINT_STATUS], SZ_UNTIL) - if SZ_STATE_STATUS in status: - convert_until(status[SZ_STATE_STATUS], SZ_UNTIL) - - return {"status": convert_dict(status)} + return {"status": self._device_state_attrs} async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" - async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh) + await super().async_added_to_hass() + + async_dispatcher_connect(self.hass, DOMAIN, self.process_signal) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + self._device_state_attrs[self._evo_id_attr] = self._evo_device.id + + for attr in self._evo_state_attr_names: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) + + super()._handle_coordinator_update() -class EvoChild(EvoDevice): +class EvoChild(EvoEntity): """Base for any evohome-compatible child entity (DHW, zone). This includes (1 to 12) heating zones and (optionally) a DHW controller. """ - _evo_id: str # mypy hint + _evo_device: evo.HotWater | evo.Zone + _evo_id: str def __init__( - self, evo_broker: EvoBroker, evo_device: evo.HotWater | evo.Zone + self, coordinator: EvoDataUpdateCoordinator, evo_device: evo.HotWater | evo.Zone ) -> None: """Initialize an evohome-compatible child entity (DHW, zone).""" - super().__init__(evo_broker, evo_device) + super().__init__(coordinator, evo_device) self._evo_tcs = evo_device.tcs - self._schedule: dict[str, Any] = {} + self._schedule: dict[str, Any] | None = None self._setpoints: dict[str, Any] = {} @property @@ -111,101 +111,78 @@ class EvoChild(EvoDevice): assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check - if (temp := self._evo_broker.temps.get(self._evo_id)) is not None: + if (temp := self.coordinator.temps.get(self._evo_id)) is not None: # use high-precision temps if available return temp return self._evo_device.temperature @property - def setpoints(self) -> dict[str, Any]: + def setpoints(self) -> Mapping[str, Any]: """Return the current/next setpoints from the schedule. Only Zones & DHW controllers (but not the TCS) can have schedules. """ - def _dt_evo_to_aware(dt_naive: datetime, utc_offset: timedelta) -> datetime: - dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset - return dt_util.as_local(dt_aware) + this_sp_dtm, this_sp_val = self._evo_device.this_switchpoint + next_sp_dtm, next_sp_val = self._evo_device.next_switchpoint - if not (schedule := self._schedule.get("DailySchedules")): - return {} # no scheduled setpoints when {'DailySchedules': []} + key = "temp" if isinstance(self._evo_device, evo.Zone) else "state" - # get dt in the same TZ as the TCS location, so we can compare schedule times - day_time = dt_util.now().astimezone(timezone(self._evo_broker.loc_utc_offset)) - day_of_week = day_time.weekday() # for evohome, 0 is Monday - time_of_day = day_time.strftime("%H:%M:%S") - - try: - # Iterate today's switchpoints until past the current time of day... - day = schedule[day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if time_of_day > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did this setpoint start yesterday? Does the next setpoint start tomorrow? - this_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in ( - ("this", this_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ): - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = schedule[(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] - - switchpoint_time_of_day = dt_util.parse_datetime( - f"{sp_date}T{switchpoint['TimeOfDay']}" - ) - assert switchpoint_time_of_day is not None # mypy check - dt_aware = _dt_evo_to_aware( - switchpoint_time_of_day, self._evo_broker.loc_utc_offset - ) - - self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() - try: - self._setpoints[f"{key}_sp_temp"] = switchpoint[SZ_HEAT_SETPOINT] - except KeyError: - self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] - - except IndexError: - self._setpoints = {} - _LOGGER.warning( - "Failed to get setpoints, report as an issue if this error persists", - exc_info=True, - ) + self._setpoints = { + "this_sp_from": this_sp_dtm, + f"this_sp_{key}": this_sp_val, + "next_sp_from": next_sp_dtm, + f"next_sp_{key}": next_sp_val, + } return self._setpoints - async def _update_schedule(self) -> None: + async def _update_schedule(self, force_refresh: bool = False) -> None: """Get the latest schedule, if any.""" - assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check + async def get_schedule() -> None: + try: + schedule = await self.coordinator.call_client_api( + self._evo_device.get_schedule(), # type: ignore[arg-type] + request_refresh=False, + ) + except evo.InvalidScheduleError as err: + _LOGGER.warning( + "%s: Unable to retrieve a valid schedule: %s", + self._evo_device, + err, + ) + self._schedule = {} + return + else: + self._schedule = schedule or {} # mypy hint - try: - schedule = await self._evo_broker.call_client_api( - self._evo_device.get_schedule(), update_state=False + _LOGGER.debug("Schedule['%s'] = %s", self.name, schedule) + + if ( + force_refresh + or self._schedule is None + or ( + (until := self._setpoints.get("next_sp_from")) is not None + and until < datetime.now(UTC) ) - except evo.InvalidSchedule as err: - _LOGGER.warning( - "%s: Unable to retrieve a valid schedule: %s", - self._evo_device, - err, - ) - self._schedule = {} - else: - self._schedule = schedule or {} + ): # must use self._setpoints, not self.setpoints + await get_schedule() - _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) + _ = self.setpoints # update the setpoints attr - async def async_update(self) -> None: - """Get the latest state data.""" - next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") - next_sp_from_dt = dt_util.parse_datetime(next_sp_from) - if next_sp_from_dt is None or dt_util.now() >= next_sp_from_dt: - await self._update_schedule() # no schedule, or it's out-of-date + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" - self._device_state_attrs = {"setpoints": self.setpoints} + self._device_state_attrs = { + "activeFaults": self._evo_device.active_faults, + "setpoints": self._setpoints, + } + + super()._handle_coordinator_update() + + async def update_attrs(self) -> None: + """Update the entity's extra state attrs.""" + await self._update_schedule() + self._handle_coordinator_update() diff --git a/homeassistant/components/evohome/helpers.py b/homeassistant/components/evohome/helpers.py deleted file mode 100644 index 0e2de36eb47..00000000000 --- a/homeassistant/components/evohome/helpers.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for (EMEA/EU-based) Honeywell TCC systems.""" - -from __future__ import annotations - -from datetime import datetime, timedelta -from http import HTTPStatus -import logging -import re -from typing import Any - -import evohomeasync2 as evo - -from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - - -def dt_local_to_aware(dt_naive: datetime) -> datetime: - """Convert a local/naive datetime to TZ-aware.""" - dt_aware = dt_util.now() + (dt_naive - datetime.now()) - if dt_aware.microsecond >= 500000: - dt_aware += timedelta(seconds=1) - return dt_aware.replace(microsecond=0) - - -def dt_aware_to_naive(dt_aware: datetime) -> datetime: - """Convert a TZ-aware datetime to naive/local.""" - dt_naive = datetime.now() + (dt_aware - dt_util.now()) - if dt_naive.microsecond >= 500000: - dt_naive += timedelta(seconds=1) - return dt_naive.replace(microsecond=0) - - -def convert_until(status_dict: dict, until_key: str) -> None: - """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" - if until_key in status_dict and ( # only present for certain modes - dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) - ): - status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() - - -def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: - """Recursively convert a dict's keys to snake_case.""" - - def convert_key(key: str) -> str: - """Convert a string to snake_case.""" - string = re.sub(r"[\-\.\s]", "_", str(key)) - return ( - (string[0]).lower() - + re.sub( - r"[A-Z]", - lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] - string[1:], - ) - ) - - return { - (convert_key(k) if isinstance(k, str) else k): ( - convert_dict(v) if isinstance(v, dict) else v - ) - for k, v in dictionary.items() - } - - -def handle_evo_exception(err: evo.RequestFailed) -> None: - """Return False if the exception can't be ignored.""" - - try: - raise err - - except evo.AuthenticationFailed: - _LOGGER.error( - ( - "Failed to authenticate with the vendor's server. Check your username" - " and password. NB: Some special password characters that work" - " correctly via the website will not work via the web API. Message" - " is: %s" - ), - err, - ) - - except evo.RequestFailed: - if err.status is None: - _LOGGER.warning( - ( - "Unable to connect with the vendor's server. " - "Check your network and the vendor's service status page. " - "Message is: %s" - ), - err, - ) - - elif err.status == HTTPStatus.SERVICE_UNAVAILABLE: - _LOGGER.warning( - "The vendor says their server is currently unavailable. " - "Check the vendor's service status page" - ) - - elif err.status == HTTPStatus.TOO_MANY_REQUESTS: - _LOGGER.warning( - ( - "The vendor's API rate limit has been exceeded. " - "If this message persists, consider increasing the %s" - ), - CONF_SCAN_INTERVAL, - ) - - else: - raise # we don't expect/handle any other Exceptions diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 22edadad7f4..823ad7be5df 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@zxdavb"], "documentation": "https://www.home-assistant.io/integrations/evohome", "iot_class": "cloud_polling", - "loggers": ["evohomeasync", "evohomeasync2"], + "loggers": ["evohome", "evohomeasync", "evohomeasync2"], "quality_scale": "legacy", - "requirements": ["evohome-async==0.4.21"] + "requirements": ["evohome-async==1.0.2"] } diff --git a/homeassistant/components/evohome/storage.py b/homeassistant/components/evohome/storage.py new file mode 100644 index 00000000000..b078c33b305 --- /dev/null +++ b/homeassistant/components/evohome/storage.py @@ -0,0 +1,118 @@ +"""Support for (EMEA/EU-based) Honeywell TCC systems.""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any, NotRequired, TypedDict + +from evohomeasync.auth import ( + SZ_SESSION_ID, + SZ_SESSION_ID_EXPIRES, + AbstractSessionManager, +) +from evohomeasync2.auth import AbstractTokenManager + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.storage import Store + +from .const import STORAGE_KEY, STORAGE_VER + + +class _SessionIdEntryT(TypedDict): + session_id: str + session_id_expires: NotRequired[str] # dt.isoformat() # TZ-aware + + +class _TokenStoreT(TypedDict): + username: str + refresh_token: str + access_token: str + access_token_expires: str # dt.isoformat() # TZ-aware + session_id: NotRequired[str] + session_id_expires: NotRequired[str] # dt.isoformat() # TZ-aware + + +class TokenManager(AbstractTokenManager, AbstractSessionManager): + """A token manager that uses a cache file to store the tokens.""" + + def __init__( + self, + hass: HomeAssistant, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialise the token manager.""" + super().__init__(*args, **kwargs) + + self._store = Store(hass, STORAGE_VER, STORAGE_KEY) # type: ignore[var-annotated] + self._store_initialized = False # True once cache loaded first time + + async def get_access_token(self) -> str: + """Return a valid access token. + + If the cached entry is not valid, will fetch a new access token. + """ + + if not self._store_initialized: + await self._load_cache_from_store() + + return await super().get_access_token() + + async def get_session_id(self) -> str: + """Return a valid session id. + + If the cached entry is not valid, will fetch a new session id. + """ + + if not self._store_initialized: + await self._load_cache_from_store() + + return await super().get_session_id() + + async def _load_cache_from_store(self) -> None: + """Load the user entry from the cache. + + Assumes single reader/writer. Reads only once, at initialization. + """ + + cache: _TokenStoreT = await self._store.async_load() or {} # type: ignore[assignment] + self._store_initialized = True + + if not cache or cache["username"] != self._client_id: + return + + if SZ_SESSION_ID in cache: + self._import_session_id(cache) # type: ignore[arg-type] + self._import_access_token(cache) + + def _import_session_id(self, session: _SessionIdEntryT) -> None: # type: ignore[override] + """Extract the session id from a (serialized) dictionary.""" + # base class method overridden because session_id_expired is NotRequired here + + self._session_id = session[SZ_SESSION_ID] + + session_id_expires = session.get(SZ_SESSION_ID_EXPIRES) + if session_id_expires is None: + self._session_id_expires = datetime.now(tz=UTC) + timedelta(minutes=15) + else: + self._session_id_expires = datetime.fromisoformat(session_id_expires) + + async def save_access_token(self) -> None: # an abstractmethod + """Save the access token (and expiry dtm, refresh token) to the cache.""" + await self.save_cache_to_store() + + async def save_session_id(self) -> None: # an abstractmethod + """Save the session id (and expiry dtm) to the cache.""" + await self.save_cache_to_store() + + async def save_cache_to_store(self) -> None: + """Save the access token (and session id, if any) to the cache. + + Assumes a single reader/writer. Writes whenever new data has been fetched. + """ + + cache = {"username": self._client_id} | self._export_access_token() + if self._session_id: + cache |= self._export_session_id() + + await self._store.async_save(cache) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 2c3cf9de12d..7ea0fb3a2d9 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -3,17 +3,11 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any import evohomeasync2 as evo -from evohomeasync2.schema.const import ( - SZ_ACTIVE_FAULTS, - SZ_DHW_ID, - SZ_OFF, - SZ_ON, - SZ_STATE_STATUS, - SZ_TEMPERATURE_STATUS, -) +from evohomeasync2.const import SZ_STATE_STATUS, SZ_TEMPERATURE_STATUS +from evohomeasync2.schemas.const import DhwState as EvoDhwState, ZoneMode as EvoZoneMode from homeassistant.components.water_heater import ( WaterHeaterEntity, @@ -31,22 +25,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER +from . import EVOHOME_KEY +from .coordinator import EvoDataUpdateCoordinator from .entity import EvoChild -if TYPE_CHECKING: - from . import EvoBroker - - _LOGGER = logging.getLogger(__name__) STATE_AUTO = "auto" -HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: SZ_ON, STATE_OFF: SZ_OFF} +HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: EvoDhwState.ON, STATE_OFF: EvoDhwState.OFF} EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""} -STATE_ATTRS_DHW = [SZ_DHW_ID, SZ_ACTIVE_FAULTS, SZ_STATE_STATUS, SZ_TEMPERATURE_STATUS] - async def async_setup_platform( hass: HomeAssistant, @@ -58,19 +47,22 @@ async def async_setup_platform( if discovery_info is None: return - broker: EvoBroker = hass.data[DOMAIN]["broker"] + coordinator = hass.data[EVOHOME_KEY].coordinator + tcs = hass.data[EVOHOME_KEY].tcs - assert broker.tcs.hotwater is not None # mypy check + assert tcs.hotwater is not None # mypy check _LOGGER.debug( "Adding: DhwController (%s), id=%s", - broker.tcs.hotwater.TYPE, - broker.tcs.hotwater.dhwId, + tcs.hotwater.type, + tcs.hotwater.id, ) - new_entity = EvoDHW(broker, broker.tcs.hotwater) + entity = EvoDHW(coordinator, tcs.hotwater) - async_add_entities([new_entity], update_before_add=True) + async_add_entities([entity]) + + await entity.update_attrs() class EvoDHW(EvoChild, WaterHeaterEntity): @@ -81,19 +73,23 @@ class EvoDHW(EvoChild, WaterHeaterEntity): _attr_operation_list = list(HA_STATE_TO_EVO) _attr_temperature_unit = UnitOfTemperature.CELSIUS - _evo_device: evo.HotWater # mypy hint + _evo_device: evo.HotWater + _evo_id_attr = "dhw_id" + _evo_state_attr_names = (SZ_STATE_STATUS, SZ_TEMPERATURE_STATUS) - def __init__(self, evo_broker: EvoBroker, evo_device: evo.HotWater) -> None: + def __init__( + self, coordinator: EvoDataUpdateCoordinator, evo_device: evo.HotWater + ) -> None: """Initialize an evohome-compatible DHW controller.""" - super().__init__(evo_broker, evo_device) - self._evo_id = evo_device.dhwId + super().__init__(coordinator, evo_device) + self._evo_id = evo_device.id - self._attr_unique_id = evo_device.dhwId + self._attr_unique_id = evo_device.id self._attr_name = evo_device.name # is static self._attr_precision = ( - PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE ) self._attr_supported_features = ( WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE @@ -102,19 +98,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity): @property def current_operation(self) -> str | None: """Return the current operating mode (Auto, On, or Off).""" - if self._evo_device.mode == EVO_FOLLOW: + if self._evo_device.mode == EvoZoneMode.FOLLOW_SCHEDULE: return STATE_AUTO - if (device_state := self._evo_device.state) is None: - return None - return EVO_STATE_TO_HA[device_state] + return EVO_STATE_TO_HA[self._evo_device.state] @property def is_away_mode_on(self) -> bool | None: """Return True if away mode is on.""" - if self._evo_device.state is None: - return None is_off = EVO_STATE_TO_HA[self._evo_device.state] == STATE_OFF - is_permanent = self._evo_device.mode == EVO_PERMOVER + is_permanent = self._evo_device.mode == EvoZoneMode.PERMANENT_OVERRIDE return is_off and is_permanent async def async_set_operation_mode(self, operation_mode: str) -> None: @@ -123,40 +115,31 @@ class EvoDHW(EvoChild, WaterHeaterEntity): Except for Auto, the mode is only until the next SetPoint. """ if operation_mode == STATE_AUTO: - await self._evo_broker.call_client_api(self._evo_device.reset_mode()) + await self.coordinator.call_client_api(self._evo_device.reset()) else: await self._update_schedule() - until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) + until = self.setpoints.get("next_sp_from") until = dt_util.as_utc(until) if until else None if operation_mode == STATE_ON: - await self._evo_broker.call_client_api( - self._evo_device.set_on(until=until) - ) + await self.coordinator.call_client_api(self._evo_device.on(until=until)) else: # STATE_OFF - await self._evo_broker.call_client_api( - self._evo_device.set_off(until=until) + await self.coordinator.call_client_api( + self._evo_device.off(until=until) ) async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" - await self._evo_broker.call_client_api(self._evo_device.set_off()) + await self.coordinator.call_client_api(self._evo_device.off()) async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" - await self._evo_broker.call_client_api(self._evo_device.reset_mode()) + await self.coordinator.call_client_api(self._evo_device.reset()) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on.""" - await self._evo_broker.call_client_api(self._evo_device.set_on()) + await self.coordinator.call_client_api(self._evo_device.on()) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" - await self._evo_broker.call_client_api(self._evo_device.set_off()) - - async def async_update(self) -> None: - """Get the latest state data for a DHW controller.""" - await super().async_update() - - for attr in STATE_ATTRS_DHW: - self._device_state_attrs[attr] = getattr(self._evo_device, attr) + await self.coordinator.call_client_api(self._evo_device.off()) diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 602eac1f24d..b10125de67c 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -2,19 +2,15 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant from .const import PLATFORMS -from .coordinator import FileSizeCoordinator - -type FileSizeConfigEntry = ConfigEntry[FileSizeCoordinator] +from .coordinator import FileSizeConfigEntry, FileSizeCoordinator async def async_setup_entry(hass: HomeAssistant, entry: FileSizeConfigEntry) -> bool: """Set up from a config entry.""" - coordinator = FileSizeCoordinator(hass, entry.data[CONF_FILE_PATH]) + coordinator = FileSizeCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -22,6 +18,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FileSizeConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: FileSizeConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/filesize/coordinator.py b/homeassistant/components/filesize/coordinator.py index 0c2a0277434..87f59f1a53e 100644 --- a/homeassistant/components/filesize/coordinator.py +++ b/homeassistant/components/filesize/coordinator.py @@ -7,6 +7,8 @@ import logging import os import pathlib +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -15,22 +17,26 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +type FileSizeConfigEntry = ConfigEntry[FileSizeCoordinator] + class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime]]): """Filesize coordinator.""" + config_entry: FileSizeConfigEntry path: pathlib.Path - def __init__(self, hass: HomeAssistant, unresolved_path: str) -> None: + def __init__(self, hass: HomeAssistant, config_entry: FileSizeConfigEntry) -> None: """Initialize filesize coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=60), always_update=False, ) - self._unresolved_path = unresolved_path + self._unresolved_path = self.config_entry.data[CONF_FILE_PATH] def _get_full_path(self) -> pathlib.Path: """Check if path is valid, allowed and return full path.""" diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 2eb170af99d..bd8b9b6c462 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -17,9 +17,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 FileSizeConfigEntry from .const import DOMAIN -from .coordinator import FileSizeCoordinator +from .coordinator import FileSizeConfigEntry, FileSizeCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 360a0f0b210..bf5385b6f2a 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -45,7 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FireServiceRota config entry.""" await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id].websocket.stop_listener + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT].websocket.stop_listener ) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: 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/manifest.json b/homeassistant/components/fireservicerota/manifest.json index 7826115fa3f..945ef141887 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fireservicerota", "iot_class": "cloud_polling", "loggers": ["pyfireservicerota"], - "requirements": ["pyfireservicerota==0.0.43"] + "requirements": ["pyfireservicerota==0.0.46"] } 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/fitbit/__init__.py b/homeassistant/components/fitbit/__init__.py index 22d0e302d63..f2378797d8d 100644 --- a/homeassistant/components/fitbit/__init__.py +++ b/homeassistant/components/fitbit/__init__.py @@ -1,24 +1,21 @@ """The fitbit component.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow from . import api -from .const import DOMAIN, FitbitScope -from .coordinator import FitbitData, FitbitDeviceCoordinator +from .const import FitbitScope +from .coordinator import FitbitConfigEntry, FitbitData, FitbitDeviceCoordinator from .exceptions import FitbitApiException, FitbitAuthException from .model import config_from_entry_data PLATFORMS: list[Platform] = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FitbitConfigEntry) -> bool: """Set up fitbit from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -38,21 +35,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fitbit_config = config_from_entry_data(entry.data) coordinator: FitbitDeviceCoordinator | None = None if fitbit_config.is_allowed_resource(FitbitScope.DEVICE, "devices/battery"): - coordinator = FitbitDeviceCoordinator(hass, fitbit_api) + coordinator = FitbitDeviceCoordinator(hass, entry, fitbit_api) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = FitbitData( - api=fitbit_api, device_coordinator=coordinator - ) + entry.runtime_data = FitbitData(api=fitbit_api, device_coordinator=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: FitbitConfigEntry) -> 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/fitbit/coordinator.py b/homeassistant/components/fitbit/coordinator.py index 2126129d261..867723419dd 100644 --- a/homeassistant/components/fitbit/coordinator.py +++ b/homeassistant/components/fitbit/coordinator.py @@ -6,6 +6,7 @@ import datetime import logging from typing import Final +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,13 +20,25 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL: Final = datetime.timedelta(minutes=30) TIMEOUT = 10 +type FitbitConfigEntry = ConfigEntry[FitbitData] + class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]): """Coordinator for fetching fitbit devices from the API.""" - def __init__(self, hass: HomeAssistant, api: FitbitApi) -> None: + config_entry: FitbitConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: FitbitConfigEntry, api: FitbitApi + ) -> None: """Initialize FitbitDeviceCoordinator.""" - super().__init__(hass, _LOGGER, name="Fitbit", update_interval=UPDATE_INTERVAL) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name="Fitbit", + update_interval=UPDATE_INTERVAL, + ) self._api = api async def _async_update_data(self) -> dict[str, FitbitDevice]: diff --git a/homeassistant/components/fitbit/quality_scale.yaml b/homeassistant/components/fitbit/quality_scale.yaml index abf127cdb98..04ed2817a07 100644 --- a/homeassistant/components/fitbit/quality_scale.yaml +++ b/homeassistant/components/fitbit/quality_scale.yaml @@ -20,11 +20,7 @@ rules: comment: Fitbit is a polling integration that does use async events. entity-unique-id: done has-entity-name: done - runtime-data: - status: todo - comment: | - The integration uses `hass.data` for data associated with a configuration - entry and needs to be updated to use `runtime_data`. + runtime-data: done test-before-configure: done test-before-setup: done unique-config-entry: done diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index d58dad4ca67..bbb3da46e52 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -31,7 +30,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .api import FitbitApi from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem -from .coordinator import FitbitData, FitbitDeviceCoordinator +from .coordinator import FitbitConfigEntry, FitbitDeviceCoordinator from .exceptions import FitbitApiException, FitbitAuthException from .model import FitbitDevice, config_from_entry_data @@ -131,7 +130,7 @@ class FitbitSensorEntityDescription(SensorEntityDescription): def _build_device_info( - config_entry: ConfigEntry, entity_description: FitbitSensorEntityDescription + config_entry: FitbitConfigEntry, entity_description: FitbitSensorEntityDescription ) -> DeviceInfo: """Build device info for sensor entities info across devices.""" unique_id = cast(str, config_entry.unique_id) @@ -524,12 +523,12 @@ FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FitbitConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fitbit sensor platform.""" - data: FitbitData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data api = data.api # These are run serially to reuse the cached user profile, not gathered @@ -601,7 +600,7 @@ class FitbitSensor(SensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: FitbitConfigEntry, api: FitbitApi, user_profile_id: str, description: FitbitSensorEntityDescription, 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/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index d95cb1d1006..2703fc5a30e 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator: FjaraskupanCoordinator = FjaraskupanCoordinator( - hass, device, device_info + hass, entry, device, device_info ) coordinator.detection_callback(service_info) diff --git a/homeassistant/components/fjaraskupan/coordinator.py b/homeassistant/components/fjaraskupan/coordinator.py index 90b2c617239..bfea5e5f4fc 100644 --- a/homeassistant/components/fjaraskupan/coordinator.py +++ b/homeassistant/components/fjaraskupan/coordinator.py @@ -21,6 +21,7 @@ from homeassistant.components.bluetooth import ( async_address_present, async_ble_device_from_address, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -64,8 +65,14 @@ class UnableToConnect(HomeAssistantError): class FjaraskupanCoordinator(DataUpdateCoordinator[State]): """Update coordinator for each device.""" + config_entry: ConfigEntry + def __init__( - self, hass: HomeAssistant, device: Device, device_info: DeviceInfo + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + device: Device, + device_info: DeviceInfo, ) -> None: """Initialize the coordinator.""" self.device = device @@ -73,7 +80,11 @@ class FjaraskupanCoordinator(DataUpdateCoordinator[State]): self._refresh_was_scheduled = False super().__init__( - hass, _LOGGER, name="Fjäråskupan", update_interval=timedelta(seconds=120) + hass, + _LOGGER, + config_entry=config_entry, + name="Fjäråskupan", + update_interval=timedelta(seconds=120), ) async def _async_refresh( diff --git a/homeassistant/components/flexit_bacnet/__init__.py b/homeassistant/components/flexit_bacnet/__init__.py index 6b42310d181..b0ebc5a40fd 100644 --- a/homeassistant/components/flexit_bacnet/__init__.py +++ b/homeassistant/components/flexit_bacnet/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE_ID, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -21,9 +21,7 @@ PLATFORMS: list[Platform] = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flexit Nordic (BACnet) from a config entry.""" - device_id = entry.data[CONF_DEVICE_ID] - - coordinator = FlexitCoordinator(hass, device_id) + coordinator = FlexitCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/flexit_bacnet/coordinator.py b/homeassistant/components/flexit_bacnet/coordinator.py index 79f3b6a05ad..f723117c9ef 100644 --- a/homeassistant/components/flexit_bacnet/coordinator.py +++ b/homeassistant/components/flexit_bacnet/coordinator.py @@ -23,12 +23,13 @@ class FlexitCoordinator(DataUpdateCoordinator[FlexitBACnet]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, device_id: str) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize my coordinator.""" super().__init__( hass, _LOGGER, - name=f"{DOMAIN}_{device_id}", + config_entry=config_entry, + name=f"{DOMAIN}_{config_entry.data[CONF_DEVICE_ID]}", update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/flexit_bacnet/icons.json b/homeassistant/components/flexit_bacnet/icons.json index 7ce8b116a27..a0c5ccd5a6e 100644 --- a/homeassistant/components/flexit_bacnet/icons.json +++ b/homeassistant/components/flexit_bacnet/icons.json @@ -30,6 +30,9 @@ }, "home_supply_fan_setpoint": { "default": "mdi:fan-plus" + }, + "fireplace_mode_runtime": { + "default": "mdi:fireplace" } }, "switch": { @@ -38,6 +41,12 @@ "state": { "off": "mdi:radiator-off" } + }, + "fireplace_mode": { + "default": "mdi:fireplace", + "state": { + "off": "mdi:fireplace-off" + } } } } diff --git a/homeassistant/components/flexit_bacnet/manifest.json b/homeassistant/components/flexit_bacnet/manifest.json index 40390162ce6..6f6b094c950 100644 --- a/homeassistant/components/flexit_bacnet/manifest.json +++ b/homeassistant/components/flexit_bacnet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flexit_bacnet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["flexit_bacnet==2.2.1"] + "requirements": ["flexit_bacnet==2.2.3"] } diff --git a/homeassistant/components/flexit_bacnet/number.py b/homeassistant/components/flexit_bacnet/number.py index 6e405e8e8ac..30df5370868 100644 --- a/homeassistant/components/flexit_bacnet/number.py +++ b/homeassistant/components/flexit_bacnet/number.py @@ -14,7 +14,7 @@ from homeassistant.components.number import ( NumberMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,6 +26,9 @@ from .entity import FlexitEntity _MAX_FAN_SETPOINT = 100 _MIN_FAN_SETPOINT = 30 +_MAX_RUNTIME_DURATION = 360 +_MIN_RUNTIME_DURATION = 1 + @dataclass(kw_only=True, frozen=True) class FlexitNumberEntityDescription(NumberEntityDescription): @@ -176,6 +179,18 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = ( native_max_value_fn=lambda _: _MAX_FAN_SETPOINT, native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_away), ), + FlexitNumberEntityDescription( + key="fireplace_mode_runtime", + translation_key="fireplace_mode_runtime", + device_class=NumberDeviceClass.DURATION, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fireplace_mode_runtime, + set_native_value_fn=lambda device: device.set_fireplace_mode_runtime, + native_unit_of_measurement=UnitOfTime.MINUTES, + native_max_value_fn=lambda _: _MAX_RUNTIME_DURATION, + native_min_value_fn=lambda _: _MIN_RUNTIME_DURATION, + ), ) diff --git a/homeassistant/components/flexit_bacnet/strings.json b/homeassistant/components/flexit_bacnet/strings.json index 7f763674d00..8888b02a3ef 100644 --- a/homeassistant/components/flexit_bacnet/strings.json +++ b/homeassistant/components/flexit_bacnet/strings.json @@ -52,6 +52,9 @@ }, "home_supply_fan_setpoint": { "name": "Home supply fan setpoint" + }, + "fireplace_mode_runtime": { + "name": "Fireplace mode runtime" } }, "sensor": { @@ -104,6 +107,9 @@ "switch": { "electric_heater": { "name": "Electric heater" + }, + "fireplace_mode": { + "name": "Fireplace mode" } } } diff --git a/homeassistant/components/flexit_bacnet/switch.py b/homeassistant/components/flexit_bacnet/switch.py index c58e35cda75..7f12a7524b6 100644 --- a/homeassistant/components/flexit_bacnet/switch.py +++ b/homeassistant/components/flexit_bacnet/switch.py @@ -40,6 +40,13 @@ SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = ( turn_on_fn=lambda data: data.enable_electric_heater(), turn_off_fn=lambda data: data.disable_electric_heater(), ), + FlexitSwitchEntityDescription( + key="fireplace_mode", + translation_key="fireplace_mode", + is_on_fn=lambda data: data.fireplace_ventilation_status, + turn_on_fn=lambda data: data.trigger_fireplace_mode(), + turn_off_fn=lambda data: data.trigger_fireplace_mode(), + ), ) diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 3ffddee1c7d..ad772c06b2e 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -9,7 +9,6 @@ from pyflick import FlickAPI from pyflick.authentication import SimpleFlickAuth from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, @@ -35,9 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> boo """Set up Flick Electric from a config entry.""" auth = HassFlickAuth(hass, entry) - coordinator = FlickElectricDataCoordinator( - hass, FlickAPI(auth), entry.data[CONF_SUPPLY_NODE_REF] - ) + coordinator = FlickElectricDataCoordinator(hass, entry, FlickAPI(auth)) await coordinator.async_config_entry_first_refresh() @@ -53,7 +50,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> bo return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, config_entry: FlickConfigEntry +) -> bool: """Migrate old entry.""" _LOGGER.debug( "Migrating configuration from version %s.%s", diff --git a/homeassistant/components/flick_electric/coordinator.py b/homeassistant/components/flick_electric/coordinator.py index 474efc5297d..114b364635f 100644 --- a/homeassistant/components/flick_electric/coordinator.py +++ b/homeassistant/components/flick_electric/coordinator.py @@ -13,6 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import CONF_SUPPLY_NODE_REF + _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=5) @@ -23,17 +25,23 @@ type FlickConfigEntry = ConfigEntry[FlickElectricDataCoordinator] class FlickElectricDataCoordinator(DataUpdateCoordinator[FlickPrice]): """Coordinator for flick power price.""" + config_entry: FlickConfigEntry + def __init__( - self, hass: HomeAssistant, api: FlickAPI, supply_node_ref: str + self, + hass: HomeAssistant, + config_entry: FlickConfigEntry, + api: FlickAPI, ) -> None: """Initialize FlickElectricDataCoordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Flick Electric", update_interval=SCAN_INTERVAL, ) - self.supply_node_ref = supply_node_ref + self.supply_node_ref = config_entry.data[CONF_SUPPLY_NODE_REF] self._api = api async def _async_update_data(self) -> FlickPrice: diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 99bddb5a0d0..81e61f2554a 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -1,7 +1,6 @@ """The Flipr integration.""" from collections import Counter -from dataclasses import dataclass import logging from flipr_api import FliprAPIRestClient @@ -13,24 +12,18 @@ from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import issue_registry as ir from .const import DOMAIN -from .coordinator import FliprDataUpdateCoordinator, FliprHubDataUpdateCoordinator +from .coordinator import ( + FliprConfigEntry, + FliprData, + FliprDataUpdateCoordinator, + FliprHubDataUpdateCoordinator, +) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) -@dataclass -class FliprData: - """The Flipr data class.""" - - flipr_coordinators: list[FliprDataUpdateCoordinator] - hub_coordinators: list[FliprHubDataUpdateCoordinator] - - -type FliprConfigEntry = ConfigEntry[FliprData] - - async def async_setup_entry(hass: HomeAssistant, entry: FliprConfigEntry) -> bool: """Set up flipr from a config entry.""" @@ -50,13 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: FliprConfigEntry) -> boo flipr_coordinators = [] for flipr_id in ids["flipr"]: - flipr_coordinator = FliprDataUpdateCoordinator(hass, client, flipr_id) + flipr_coordinator = FliprDataUpdateCoordinator(hass, entry, client, flipr_id) await flipr_coordinator.async_config_entry_first_refresh() flipr_coordinators.append(flipr_coordinator) hub_coordinators = [] for hub_id in ids["hub"]: - hub_coordinator = FliprHubDataUpdateCoordinator(hass, client, hub_id) + hub_coordinator = FliprHubDataUpdateCoordinator(hass, entry, client, hub_id) await hub_coordinator.async_config_entry_first_refresh() hub_coordinators.append(hub_coordinator) diff --git a/homeassistant/components/flipr/binary_sensor.py b/homeassistant/components/flipr/binary_sensor.py index cc6a9d36abc..07357b81af0 100644 --- a/homeassistant/components/flipr/binary_sensor.py +++ b/homeassistant/components/flipr/binary_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FliprConfigEntry +from .coordinator import FliprConfigEntry from .entity import FliprEntity BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( diff --git a/homeassistant/components/flipr/coordinator.py b/homeassistant/components/flipr/coordinator.py index 12fd174fe7d..0d86b43711a 100644 --- a/homeassistant/components/flipr/coordinator.py +++ b/homeassistant/components/flipr/coordinator.py @@ -1,5 +1,6 @@ """DataUpdateCoordinator for flipr integration.""" +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any @@ -14,13 +15,28 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda _LOGGER = logging.getLogger(__name__) +@dataclass +class FliprData: + """The Flipr data class.""" + + flipr_coordinators: list["FliprDataUpdateCoordinator"] + hub_coordinators: list["FliprHubDataUpdateCoordinator"] + + +type FliprConfigEntry = ConfigEntry[FliprData] + + class BaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Parent class to hold Flipr and Hub data retrieval.""" - config_entry: ConfigEntry + config_entry: FliprConfigEntry def __init__( - self, hass: HomeAssistant, client: FliprAPIRestClient, flipr_or_hub_id: str + self, + hass: HomeAssistant, + config_entry: FliprConfigEntry, + client: FliprAPIRestClient, + flipr_or_hub_id: str, ) -> None: """Initialize.""" self.device_id = flipr_or_hub_id @@ -29,6 +45,7 @@ class BaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"Flipr or Hub data measure for {self.device_id}", update_interval=timedelta(minutes=15), ) diff --git a/homeassistant/components/flipr/select.py b/homeassistant/components/flipr/select.py index b8a8f0db60a..79515be6ed4 100644 --- a/homeassistant/components/flipr/select.py +++ b/homeassistant/components/flipr/select.py @@ -6,7 +6,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FliprConfigEntry +from .coordinator import FliprConfigEntry from .entity import FliprEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index ba863718182..2594186f24a 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import PERCENTAGE, UnitOfElectricPotential, UnitOfTempe from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FliprConfigEntry +from .coordinator import FliprConfigEntry from .entity import FliprEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( diff --git a/homeassistant/components/flipr/switch.py b/homeassistant/components/flipr/switch.py index 65e729ec280..03df7f34d12 100644 --- a/homeassistant/components/flipr/switch.py +++ b/homeassistant/components/flipr/switch.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FliprConfigEntry +from .coordinator import FliprConfigEntry from .entity import FliprEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index b619df91d59..6a497f5140d 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -37,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Flo user information with locations: %s", user_info) hass.data[DOMAIN][entry.entry_id]["devices"] = devices = [ - FloDeviceDataUpdateCoordinator(hass, client, location["id"], device["id"]) + FloDeviceDataUpdateCoordinator( + hass, entry, client, location["id"], device["id"] + ) for location in user_info["locations"] for device in location["devices"] ] diff --git a/homeassistant/components/flo/coordinator.py b/homeassistant/components/flo/coordinator.py index d0dd38bd490..f5dc34a50cd 100644 --- a/homeassistant/components/flo/coordinator.py +++ b/homeassistant/components/flo/coordinator.py @@ -10,6 +10,7 @@ from aioflo.api import API from aioflo.errors import RequestError from orjson import JSONDecodeError +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 @@ -20,10 +21,16 @@ from .const import DOMAIN as FLO_DOMAIN, LOGGER class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Flo device object.""" + config_entry: ConfigEntry _failure_count: int = 0 def __init__( - self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + api_client: API, + location_id: str, + device_id: str, ) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass @@ -36,6 +43,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): super().__init__( hass, LOGGER, + config_entry=config_entry, name=f"{FLO_DOMAIN}-{device_id}", update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index d91c6b175cf..d229665ca62 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 ConfigEntryState from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -24,28 +24,24 @@ 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}), }, ) def _setup_entry( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: FlumeConfigEntry ) -> tuple[FlumeAuth, FlumeDeviceList, Session]: """Config entry set up in executor.""" config = entry.data @@ -76,22 +72,22 @@ 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( _setup_entry, hass, entry ) notification_coordinator = FlumeNotificationDataUpdateCoordinator( - hass=hass, auth=flume_auth + hass=hass, config_entry=entry, 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..cb0add90443 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,21 +68,21 @@ 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 ] = [] connection_coordinator = FlumeDeviceConnectionUpdateCoordinator( - hass=hass, flume_devices=flume_devices + hass=hass, config_entry=config_entry, 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..1dabf5726b2 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,13 +22,34 @@ 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.""" - def __init__(self, hass: HomeAssistant, flume_device: FlumeData) -> None: + config_entry: FlumeConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: FlumeConfigEntry, + flume_device: FlumeData, + ) -> None: """Initialize the Coordinator.""" super().__init__( hass, + config_entry=config_entry, name=DOMAIN, logger=_LOGGER, update_interval=DEVICE_SCAN_INTERVAL, @@ -49,10 +73,18 @@ class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): class FlumeDeviceConnectionUpdateCoordinator(DataUpdateCoordinator[None]): """Date update coordinator to read connected status from Devices endpoint.""" - def __init__(self, hass: HomeAssistant, flume_devices: FlumeDeviceList) -> None: + config_entry: FlumeConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: FlumeConfigEntry, + flume_devices: FlumeDeviceList, + ) -> None: """Initialize the Coordinator.""" super().__init__( hass, + config_entry=config_entry, name=DOMAIN, logger=_LOGGER, update_interval=DEVICE_CONNECTION_SCAN_INTERVAL, @@ -80,10 +112,15 @@ class FlumeDeviceConnectionUpdateCoordinator(DataUpdateCoordinator[None]): class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for flume notifications.""" - def __init__(self, hass: HomeAssistant, auth: FlumeAuth) -> None: + config_entry: FlumeConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: FlumeConfigEntry, auth: FlumeAuth + ) -> None: """Initialize the Coordinator.""" super().__init__( hass, + config_entry=config_entry, name=DOMAIN, logger=_LOGGER, update_interval=NOTIFICATION_SCAN_INTERVAL, diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 96395e5403f..aea0aa60093 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) @@ -137,7 +132,7 @@ async def async_setup_entry( flume_device = flume_datas[device_id] coordinator = FlumeDeviceDataUpdateCoordinator( - hass=hass, flume_device=flume_device + hass=hass, config_entry=config_entry, flume_device=flume_device ) flume_entity_list.extend( diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index a473387a513..a879d894bcc 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -24,17 +24,19 @@ REQUEST_REFRESH_DELAY: Final = 2.0 class FluxLedUpdateCoordinator(DataUpdateCoordinator[None]): """DataUpdateCoordinator to gather data for a specific flux_led device.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry ) -> None: """Initialize DataUpdateCoordinator to gather data for specific device.""" self.device = device self.title = entry.title - self.entry = entry self.force_next_update = False super().__init__( hass, _LOGGER, + config_entry=entry, name=self.device.ipaddr, update_interval=timedelta(seconds=10), # We don't want an immediate refresh since the device diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index bcf7bfff9ed..f9b87dbb8c1 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -89,7 +89,9 @@ class FluxEntity(CoordinatorEntity[FluxLedUpdateCoordinator]): self._attr_unique_id = f"{base_unique_id}_{key}" else: self._attr_unique_id = base_unique_id - self._attr_device_info = _async_device_info(self._device, coordinator.entry) + self._attr_device_info = _async_device_info( + self._device, coordinator.config_entry + ) async def _async_ensure_device_on(self) -> None: """Turn the device on if it needs to be turned on before a command.""" diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 3809e73147a..33329ebb3f3 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -141,7 +141,7 @@ class FluxICTypeSelect(FluxConfigSelect): async def async_select_option(self, option: str) -> None: """Change the ic type.""" await self._device.async_set_device_config(ic_type=option) - await _async_delayed_reload(self.hass, self.coordinator.entry) + await _async_delayed_reload(self.hass, self.coordinator.config_entry) class FluxWiringsSelect(FluxConfigSelect): @@ -184,7 +184,7 @@ class FluxOperatingModesSelect(FluxConfigSelect): async def async_select_option(self, option: str) -> None: """Change the ic type.""" await self._device.async_set_device_config(operating_mode=option) - await _async_delayed_reload(self.hass, self.coordinator.entry) + await _async_delayed_reload(self.hass, self.coordinator.config_entry) class FluxRemoteConfigSelect(FluxConfigSelect): diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 00be13f1235..171341f7226 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -12,14 +11,14 @@ from .const import ( CONF_DAMPING_MORNING, CONF_MODULES_POWER, ) -from .coordinator import ForecastSolarDataUpdateCoordinator +from .coordinator import ForecastSolarConfigEntry, ForecastSolarDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -type ForecastSolarConfigEntry = ConfigEntry[ForecastSolarDataUpdateCoordinator] - -async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, entry: ForecastSolarConfigEntry +) -> bool: """Migrate old config entry.""" if entry.version == 1: @@ -53,11 +52,15 @@ async def async_setup_entry( return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: ForecastSolarConfigEntry +) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_update_options( + hass: HomeAssistant, entry: ForecastSolarConfigEntry +) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/forecast_solar/coordinator.py b/homeassistant/components/forecast_solar/coordinator.py index c9c062a0c88..efed954e490 100644 --- a/homeassistant/components/forecast_solar/coordinator.py +++ b/homeassistant/components/forecast_solar/coordinator.py @@ -23,15 +23,16 @@ from .const import ( LOGGER, ) +type ForecastSolarConfigEntry = ConfigEntry[ForecastSolarDataUpdateCoordinator] + class ForecastSolarDataUpdateCoordinator(DataUpdateCoordinator[Estimate]): """The Forecast.Solar Data Update Coordinator.""" - config_entry: ConfigEntry + config_entry: ForecastSolarConfigEntry - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ForecastSolarConfigEntry) -> None: """Initialize the Forecast.Solar coordinator.""" - self.config_entry = entry # Our option flow may cause it to be an empty string, # this if statement is here to catch that. @@ -61,7 +62,13 @@ class ForecastSolarDataUpdateCoordinator(DataUpdateCoordinator[Estimate]): if api_key is not None: update_interval = timedelta(minutes=30) - 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 _async_update_data(self) -> Estimate: """Fetch Forecast.Solar estimates.""" 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/coordinator.py b/homeassistant/components/forked_daapd/coordinator.py new file mode 100644 index 00000000000..7a03a9075ed --- /dev/null +++ b/homeassistant/components/forked_daapd/coordinator.py @@ -0,0 +1,142 @@ +"""Support forked_daapd media player.""" + +from __future__ import annotations + +import asyncio +import logging + +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + SIGNAL_ADD_ZONES, + SIGNAL_UPDATE_DATABASE, + SIGNAL_UPDATE_MASTER, + SIGNAL_UPDATE_OUTPUTS, + SIGNAL_UPDATE_PLAYER, + SIGNAL_UPDATE_QUEUE, +) + +_LOGGER = logging.getLogger(__name__) + +WS_NOTIFY_EVENT_TYPES = ["player", "outputs", "volume", "options", "queue", "database"] +WEBSOCKET_RECONNECT_TIME = 30 # seconds + + +class ForkedDaapdUpdater: + """Manage updates for the forked-daapd device.""" + + def __init__(self, hass, api, entry_id): + """Initialize.""" + self.hass = hass + self._api = api + self.websocket_handler = None + self._all_output_ids = set() + self._entry_id = entry_id + + async def async_init(self): + """Perform async portion of class initialization.""" + if not (server_config := await self._api.get_request("config")): + raise PlatformNotReady + if websocket_port := server_config.get("websocket_port"): + self.websocket_handler = asyncio.create_task( + self._api.start_websocket_handler( + websocket_port, + WS_NOTIFY_EVENT_TYPES, + self._update, + WEBSOCKET_RECONNECT_TIME, + self._disconnected_callback, + ) + ) + else: + _LOGGER.error("Invalid websocket port") + + async def _disconnected_callback(self): + """Send update signals when the websocket gets disconnected.""" + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), False + ) + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), [] + ) + + async def _update(self, update_types): + """Private update method.""" + update_types = set(update_types) + update_events = {} + _LOGGER.debug("Updating %s", update_types) + if ( + "queue" in update_types + ): # update queue, queue before player for async_play_media + if queue := await self._api.get_request("queue"): + update_events["queue"] = asyncio.Event() + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_QUEUE.format(self._entry_id), + queue, + update_events["queue"], + ) + # order of below don't matter + if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs + if outputs := await self._api.get_request("outputs"): + outputs = outputs["outputs"] + update_events["outputs"] = ( + asyncio.Event() + ) # only for master, zones should ignore + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), + outputs, + update_events["outputs"], + ) + self._add_zones(outputs) + if not {"database"}.isdisjoint(update_types): + pipes, playlists = await asyncio.gather( + self._api.get_pipes(), self._api.get_playlists() + ) + update_events["database"] = asyncio.Event() + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_DATABASE.format(self._entry_id), + pipes, + playlists, + update_events["database"], + ) + if not {"update", "config"}.isdisjoint(update_types): # not supported + _LOGGER.debug("update/config notifications neither requested nor supported") + if not {"player", "options", "volume"}.isdisjoint( + update_types + ): # update player + if player := await self._api.get_request("player"): + update_events["player"] = asyncio.Event() + if update_events.get("queue"): + await update_events[ + "queue" + ].wait() # make sure queue done before player for async_play_media + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_PLAYER.format(self._entry_id), + player, + update_events["player"], + ) + if update_events: + await asyncio.wait( + [asyncio.create_task(event.wait()) for event in update_events.values()] + ) # make sure callbacks done before update + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), True + ) + + def _add_zones(self, outputs): + outputs_to_add = [] + for output in outputs: + if output["id"] not in self._all_output_ids: + self._all_output_ids.add(output["id"]) + outputs_to_add.append(output) + if outputs_to_add: + async_dispatcher_send( + self.hass, + SIGNAL_ADD_ZONES.format(self._entry_id), + self._api, + outputs_to_add, + ) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index b8b544c1a2c..8e61df3de45 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -31,7 +31,6 @@ from homeassistant.components.spotify import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -58,7 +57,6 @@ from .const import ( DEFAULT_UNMUTE_VOLUME, DOMAIN, FD_NAME, - HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY, KNOWN_PIPES, PIPE_FUNCTION_MAP, @@ -76,12 +74,10 @@ from .const import ( SUPPORTED_FEATURES_ZONE, TTS_TIMEOUT, ) +from .coordinator import ForkedDaapdUpdater _LOGGER = logging.getLogger(__name__) -WS_NOTIFY_EVENT_TYPES = ["player", "outputs", "volume", "options", "queue", "database"] -WEBSOCKET_RECONNECT_TIME = 30 # seconds - async def async_setup_entry( hass: HomeAssistant, @@ -110,19 +106,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 @@ -901,122 +894,3 @@ class ForkedDaapdMaster(MediaPlayerEntity): if url := result.get("artwork_url"): return await self._async_fetch_image(self.api.full_url(url)) return None, None - - -class ForkedDaapdUpdater: - """Manage updates for the forked-daapd device.""" - - def __init__(self, hass, api, entry_id): - """Initialize.""" - self.hass = hass - self._api = api - self.websocket_handler = None - self._all_output_ids = set() - self._entry_id = entry_id - - async def async_init(self): - """Perform async portion of class initialization.""" - if not (server_config := await self._api.get_request("config")): - raise PlatformNotReady - if websocket_port := server_config.get("websocket_port"): - self.websocket_handler = asyncio.create_task( - self._api.start_websocket_handler( - websocket_port, - WS_NOTIFY_EVENT_TYPES, - self._update, - WEBSOCKET_RECONNECT_TIME, - self._disconnected_callback, - ) - ) - else: - _LOGGER.error("Invalid websocket port") - - async def _disconnected_callback(self): - """Send update signals when the websocket gets disconnected.""" - async_dispatcher_send( - self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), False - ) - async_dispatcher_send( - self.hass, SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), [] - ) - - async def _update(self, update_types): - """Private update method.""" - update_types = set(update_types) - update_events = {} - _LOGGER.debug("Updating %s", update_types) - if ( - "queue" in update_types - ): # update queue, queue before player for async_play_media - if queue := await self._api.get_request("queue"): - update_events["queue"] = asyncio.Event() - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_QUEUE.format(self._entry_id), - queue, - update_events["queue"], - ) - # order of below don't matter - if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs - if outputs := await self._api.get_request("outputs"): - outputs = outputs["outputs"] - update_events["outputs"] = ( - asyncio.Event() - ) # only for master, zones should ignore - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), - outputs, - update_events["outputs"], - ) - self._add_zones(outputs) - if not {"database"}.isdisjoint(update_types): - pipes, playlists = await asyncio.gather( - self._api.get_pipes(), self._api.get_playlists() - ) - update_events["database"] = asyncio.Event() - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_DATABASE.format(self._entry_id), - pipes, - playlists, - update_events["database"], - ) - if not {"update", "config"}.isdisjoint(update_types): # not supported - _LOGGER.debug("update/config notifications neither requested nor supported") - if not {"player", "options", "volume"}.isdisjoint( - update_types - ): # update player - if player := await self._api.get_request("player"): - update_events["player"] = asyncio.Event() - if update_events.get("queue"): - await update_events[ - "queue" - ].wait() # make sure queue done before player for async_play_media - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_PLAYER.format(self._entry_id), - player, - update_events["player"], - ) - if update_events: - await asyncio.wait( - [asyncio.create_task(event.wait()) for event in update_events.values()] - ) # make sure callbacks done before update - async_dispatcher_send( - self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), True - ) - - def _add_zones(self, outputs): - outputs_to_add = [] - for output in outputs: - if output["id"] not in self._all_output_ids: - self._all_output_ids.add(output["id"]) - outputs_to_add.append(output) - if outputs_to_add: - async_dispatcher_send( - self.hass, - SIGNAL_ADD_ZONES.format(self._entry_id), - self._api, - outputs_to_add, - ) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 09df989447a..9643f333bb5 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -2,7 +2,6 @@ from libpyfoscam import FoscamCamera -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -14,13 +13,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from .config_flow import DEFAULT_RTSP_PORT -from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET -from .coordinator import FoscamCoordinator +from .const import CONF_RTSP_PORT, LOGGER +from .coordinator import FoscamConfigEntry, FoscamCoordinator PLATFORMS = [Platform.CAMERA, Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool: """Set up foscam from a config entry.""" session = FoscamCamera( @@ -30,11 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], verbose=False, ) - coordinator = FoscamCoordinator(hass, session) + coordinator = FoscamCoordinator(hass, entry, session) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator # Migrate to correct unique IDs for switches await async_migrate_entities(hass, entry) @@ -44,20 +43,12 @@ 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: FoscamConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - if not hass.data[DOMAIN]: - hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ) - hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool: """Migrate old entry.""" LOGGER.debug("Migrating from version %s", entry.version) @@ -97,7 +88,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_migrate_entities(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_migrate_entities(hass: HomeAssistant, entry: FoscamConfigEntry) -> None: """Migrate old entry.""" @callback diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 075848f6ffb..ed5ba1d4c21 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -7,21 +7,13 @@ import asyncio import voluptuous as vol from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - CONF_RTSP_PORT, - CONF_STREAM, - DOMAIN, - LOGGER, - SERVICE_PTZ, - SERVICE_PTZ_PRESET, -) -from .coordinator import FoscamCoordinator +from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET +from .coordinator import FoscamConfigEntry, FoscamCoordinator from .entity import FoscamEntity DIR_UP = "up" @@ -56,7 +48,7 @@ PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FoscamConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add a Foscam IP camera from a config entry.""" @@ -89,7 +81,7 @@ async def async_setup_entry( "async_perform_ptz_preset", ) - coordinator: FoscamCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities([HassFoscamCamera(coordinator, config_entry)]) @@ -103,7 +95,7 @@ class HassFoscamCamera(FoscamEntity, Camera): def __init__( self, coordinator: FoscamCoordinator, - config_entry: ConfigEntry, + config_entry: FoscamConfigEntry, ) -> None: """Initialize a Foscam camera.""" super().__init__(coordinator, config_entry.entry_id) diff --git a/homeassistant/components/foscam/coordinator.py b/homeassistant/components/foscam/coordinator.py index e7a8abf7d30..92eb7615e2a 100644 --- a/homeassistant/components/foscam/coordinator.py +++ b/homeassistant/components/foscam/coordinator.py @@ -6,11 +6,14 @@ from typing import Any from libpyfoscam import FoscamCamera +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER +type FoscamConfigEntry = ConfigEntry[FoscamCoordinator] + class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Foscam coordinator.""" @@ -18,12 +21,14 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): def __init__( self, hass: HomeAssistant, + entry: FoscamConfigEntry, session: FoscamCamera, ) -> None: """Initialize my coordinator.""" super().__init__( hass, LOGGER, + config_entry=entry, name=DOMAIN, update_interval=timedelta(seconds=30), ) diff --git a/homeassistant/components/foscam/switch.py b/homeassistant/components/foscam/switch.py index dfc51aaa064..189271d2746 100644 --- a/homeassistant/components/foscam/switch.py +++ b/homeassistant/components/foscam/switch.py @@ -5,24 +5,23 @@ from __future__ import annotations from typing import Any from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FoscamCoordinator -from .const import DOMAIN, LOGGER +from .const import LOGGER +from .coordinator import FoscamConfigEntry, FoscamCoordinator from .entity import FoscamEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FoscamConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up foscam switch from a config entry.""" - coordinator: FoscamCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data await coordinator.async_config_entry_first_refresh() @@ -36,7 +35,7 @@ class FoscamSleepSwitch(FoscamEntity, SwitchEntity): def __init__( self, coordinator: FoscamCoordinator, - config_entry: ConfigEntry, + config_entry: FoscamConfigEntry, ) -> None: """Initialize a Foscam Sleep Switch.""" super().__init__(coordinator, config_entry.entry_id) diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index c14c2f5ae36..9ce7701216c 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -2,17 +2,12 @@ from __future__ import annotations -import logging from typing import Final -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator PLATFORMS: Final[list[Platform]] = [ Platform.BINARY_SENSOR, @@ -26,32 +21,27 @@ PLATFORMS: Final[list[Platform]] = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FreedomproConfigEntry) -> bool: """Set up Freedompro from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - api_key = entry.data[CONF_API_KEY] - - coordinator = FreedomproDataUpdateCoordinator(hass, api_key) + coordinator = FreedomproDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.data[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: FreedomproConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def update_listener( + hass: HomeAssistant, config_entry: FreedomproConfigEntry +) -> None: """Update listener.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index ccea5faf41f..840150e807d 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -6,14 +6,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator DEVICE_CLASS_MAP = { "smokeSensor": BinarySensorDeviceClass.SMOKE, @@ -33,10 +32,12 @@ SUPPORTED_SENSORS = {"smokeSensor", "occupancySensor", "motionSensor", "contactS async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro binary_sensor.""" - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index a5b0144ce0c..a0146dc70b3 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -24,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -44,11 +43,13 @@ SUPPORTED_HVAC_MODES = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro climate.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device( aiohttp_client.async_get_clientsession(hass), api_key, device, coordinator diff --git a/homeassistant/components/freedompro/coordinator.py b/homeassistant/components/freedompro/coordinator.py index ad76a9aaa65..23b181b2655 100644 --- a/homeassistant/components/freedompro/coordinator.py +++ b/homeassistant/components/freedompro/coordinator.py @@ -8,6 +8,9 @@ from typing import Any from pyfreedompro import get_list, get_states +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,18 +18,27 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +type FreedomproConfigEntry = ConfigEntry[FreedomproDataUpdateCoordinator] + class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): """Class to manage fetching Freedompro data API.""" - def __init__(self, hass, api_key): + def __init__(self, hass: HomeAssistant, entry: FreedomproConfigEntry) -> None: """Initialize.""" + self._hass = hass - self._api_key = api_key + self._api_key = entry.data[CONF_API_KEY] self._devices: list[dict[str, Any]] | None = None update_interval = timedelta(minutes=1) - 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 _async_update_data(self): if self._devices is None: diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 06ad5c80b6a..ee61612428c 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -11,7 +11,6 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -20,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator DEVICE_CLASS_MAP = { "windowCovering": CoverDeviceClass.BLIND, @@ -34,11 +33,13 @@ SUPPORTED_SENSORS = {"windowCovering", "gate", "garageDoor", "door", "window"} async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro cover.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index d21ede9bad3..ad520ac8eb8 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -8,7 +8,6 @@ from typing import Any from pyfreedompro import put_state from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -17,15 +16,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro fan.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( FreedomproFan(hass, api_key, device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index ab8df7ec9db..c1b2e0ea17b 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -13,7 +13,6 @@ from homeassistant.components.light import ( ColorMode, LightEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -22,15 +21,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro light.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index c429ef6aa99..70423bb9514 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -6,7 +6,6 @@ from typing import Any from pyfreedompro import put_state from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -15,15 +14,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro lock.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 3c5101e3634..eaa96ac9fed 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -15,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator DEVICE_CLASS_MAP = { "temperatureSensor": SensorDeviceClass.TEMPERATURE, @@ -41,10 +40,12 @@ SUPPORTED_SENSORS = {"temperatureSensor", "humiditySensor", "lightSensor"} async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro sensor.""" - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(device, coordinator) for device in coordinator.data diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index 91e67506173..12346825474 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -6,7 +6,6 @@ from typing import Any from pyfreedompro import put_state from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -15,15 +14,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FreedomproDataUpdateCoordinator +from .coordinator import FreedomproConfigEntry, FreedomproDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: FreedomproConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Freedompro switch.""" api_key: str = entry.data[CONF_API_KEY] - coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data 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/fujitsu_fglair/__init__.py b/homeassistant/components/fujitsu_fglair/__init__.py index 547545e4feb..699356a2e75 100644 --- a/homeassistant/components/fujitsu_fglair/__init__.py +++ b/homeassistant/components/fujitsu_fglair/__init__.py @@ -7,18 +7,15 @@ from contextlib import suppress from ayla_iot_unofficial import new_ayla_api from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from .const import API_TIMEOUT, CONF_EUROPE, CONF_REGION, REGION_DEFAULT, REGION_EU -from .coordinator import FGLairCoordinator +from .coordinator import FGLairConfigEntry, FGLairCoordinator PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] -type FGLairConfigEntry = ConfigEntry[FGLairCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool: """Set up Fujitsu HVAC (based on Ayla IOT) from a config entry.""" @@ -33,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bo timeout=API_TIMEOUT, ) - coordinator = FGLairCoordinator(hass, api) + coordinator = FGLairCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/fujitsu_fglair/climate.py b/homeassistant/components/fujitsu_fglair/climate.py index c0f5ab7dce4..5df6573e638 100644 --- a/homeassistant/components/fujitsu_fglair/climate.py +++ b/homeassistant/components/fujitsu_fglair/climate.py @@ -27,8 +27,7 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemper from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FGLairConfigEntry -from .coordinator import FGLairCoordinator +from .coordinator import FGLairConfigEntry, FGLairCoordinator from .entity import FGLairEntity HA_TO_FUJI_FAN = { diff --git a/homeassistant/components/fujitsu_fglair/coordinator.py b/homeassistant/components/fujitsu_fglair/coordinator.py index d98464e4751..8c66548973b 100644 --- a/homeassistant/components/fujitsu_fglair/coordinator.py +++ b/homeassistant/components/fujitsu_fglair/coordinator.py @@ -5,6 +5,7 @@ import logging from ayla_iot_unofficial import AylaApi, AylaAuthError from ayla_iot_unofficial.fujitsu_hvac import FujitsuHVAC +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -13,15 +14,22 @@ from .const import API_REFRESH _LOGGER = logging.getLogger(__name__) +type FGLairConfigEntry = ConfigEntry[FGLairCoordinator] + class FGLairCoordinator(DataUpdateCoordinator[dict[str, FujitsuHVAC]]): """Coordinator for Fujitsu HVAC integration.""" - def __init__(self, hass: HomeAssistant, api: AylaApi) -> None: + config_entry: FGLairConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: FGLairConfigEntry, api: AylaApi + ) -> None: """Initialize coordinator for Fujitsu HVAC integration.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Fujitsu HVAC data", update_interval=API_REFRESH, ) diff --git a/homeassistant/components/fujitsu_fglair/sensor.py b/homeassistant/components/fujitsu_fglair/sensor.py index 1426e2349ea..e095a566dcb 100644 --- a/homeassistant/components/fujitsu_fglair/sensor.py +++ b/homeassistant/components/fujitsu_fglair/sensor.py @@ -11,8 +11,7 @@ from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .climate import FGLairConfigEntry -from .coordinator import FGLairCoordinator +from .coordinator import FGLairConfigEntry, FGLairCoordinator from .entity import FGLairEntity diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 074ec3feaa0..772e7f79242 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -1,17 +1,14 @@ """The Fully Kiosk Browser integration.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -from .coordinator import FullyKioskDataUpdateCoordinator +from .coordinator import FullyKioskConfigEntry, FullyKioskDataUpdateCoordinator from .services import async_setup_services -type FullyKioskConfigEntry = ConfigEntry[FullyKioskDataUpdateCoordinator] - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, diff --git a/homeassistant/components/fully_kiosk/coordinator.py b/homeassistant/components/fully_kiosk/coordinator.py index 405f0746437..dc3640020be 100644 --- a/homeassistant/components/fully_kiosk/coordinator.py +++ b/homeassistant/components/fully_kiosk/coordinator.py @@ -14,11 +14,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_PORT, LOGGER, UPDATE_INTERVAL +type FullyKioskConfigEntry = ConfigEntry[FullyKioskDataUpdateCoordinator] + class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Fully Kiosk Browser data.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + config_entry: FullyKioskConfigEntry + + def __init__(self, hass: HomeAssistant, entry: FullyKioskConfigEntry) -> None: """Initialize.""" self.use_ssl = entry.data.get(CONF_SSL, False) self.fully = FullyKiosk( @@ -32,6 +36,7 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator): super().__init__( hass, LOGGER, + config_entry=entry, name=entry.data[CONF_HOST], update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index ab4a74c627a..1b00afc9c80 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -7,7 +7,6 @@ import logging from fyta_cli.fyta_connector import FytaConnector -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_PASSWORD, @@ -19,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.dt import async_get_time_zone from .const import CONF_EXPIRATION -from .coordinator import FytaCoordinator +from .coordinator import FytaConfigEntry, FytaCoordinator _LOGGER = logging.getLogger(__name__) @@ -28,7 +27,6 @@ PLATFORMS = [ Platform.IMAGE, Platform.SENSOR, ] -type FytaConfigEntry = ConfigEntry[FytaCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: FytaConfigEntry) -> bool: @@ -46,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FytaConfigEntry) -> bool username, password, access_token, expiration, tz, async_get_clientsession(hass) ) - coordinator = FytaCoordinator(hass, fyta) + coordinator = FytaCoordinator(hass, entry, fyta) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/fyta/binary_sensor.py b/homeassistant/components/fyta/binary_sensor.py index bcef609d01a..66e5b2feeca 100644 --- a/homeassistant/components/fyta/binary_sensor.py +++ b/homeassistant/components/fyta/binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FytaConfigEntry +from .coordinator import FytaConfigEntry from .entity import FytaPlantEntity diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index a0c42d449d5..012ed3b2af0 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING from fyta_cli.fyta_connector import FytaConnector from fyta_cli.fyta_exceptions import ( @@ -16,6 +15,7 @@ from fyta_cli.fyta_exceptions import ( ) from fyta_cli.fyta_models import Plant +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -24,22 +24,24 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_EXPIRATION, DOMAIN -if TYPE_CHECKING: - from . import FytaConfigEntry - _LOGGER = logging.getLogger(__name__) +type FytaConfigEntry = ConfigEntry[FytaCoordinator] + class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]): """Fyta custom coordinator.""" config_entry: FytaConfigEntry - def __init__(self, hass: HomeAssistant, fyta: FytaConnector) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: FytaConfigEntry, fyta: FytaConnector + ) -> None: """Initialize my coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="FYTA Coordinator", update_interval=timedelta(minutes=4), ) diff --git a/homeassistant/components/fyta/diagnostics.py b/homeassistant/components/fyta/diagnostics.py index d02f8cacfa3..d6bda70d754 100644 --- a/homeassistant/components/fyta/diagnostics.py +++ b/homeassistant/components/fyta/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import FytaConfigEntry +from .coordinator import FytaConfigEntry TO_REDACT = [ CONF_PASSWORD, diff --git a/homeassistant/components/fyta/entity.py b/homeassistant/components/fyta/entity.py index 0d0ec533c44..02cd73c54f9 100644 --- a/homeassistant/components/fyta/entity.py +++ b/homeassistant/components/fyta/entity.py @@ -6,9 +6,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import FytaConfigEntry from .const import DOMAIN -from .coordinator import FytaCoordinator +from .coordinator import FytaConfigEntry, FytaCoordinator class FytaPlantEntity(CoordinatorEntity[FytaCoordinator]): diff --git a/homeassistant/components/fyta/image.py b/homeassistant/components/fyta/image.py index f03df969dcc..4a0b32f605b 100644 --- a/homeassistant/components/fyta/image.py +++ b/homeassistant/components/fyta/image.py @@ -9,8 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FytaConfigEntry -from .coordinator import FytaCoordinator +from .coordinator import FytaConfigEntry, FytaCoordinator from .entity import FytaPlantEntity diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 254e4522819..66c96ab697b 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -25,8 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import FytaConfigEntry -from .coordinator import FytaCoordinator +from .coordinator import FytaConfigEntry, FytaCoordinator from .entity import FytaPlantEntity diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 99d751cfcc8..854e41f2d89 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -4,24 +4,24 @@ from __future__ import annotations from odp_amsterdam import ODPAmsterdam -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 .coordinator import GaragesAmsterdamDataUpdateCoordinator +from .coordinator import ( + GaragesAmsterdamConfigEntry, + GaragesAmsterdamDataUpdateCoordinator, +) PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] -type GaragesAmsterdamConfigEntry = ConfigEntry[GaragesAmsterdamDataUpdateCoordinator] - async def async_setup_entry( hass: HomeAssistant, entry: GaragesAmsterdamConfigEntry ) -> bool: """Set up Garages Amsterdam from a config entry.""" client = ODPAmsterdam(session=async_get_clientsession(hass)) - coordinator = GaragesAmsterdamDataUpdateCoordinator(hass, client) + coordinator = GaragesAmsterdamDataUpdateCoordinator(hass, entry, client) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py index b93b43e1173..cf4b29f0af8 100644 --- a/homeassistant/components/garages_amsterdam/binary_sensor.py +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -15,8 +15,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GaragesAmsterdamConfigEntry -from .coordinator import GaragesAmsterdamDataUpdateCoordinator +from .coordinator import ( + GaragesAmsterdamConfigEntry, + GaragesAmsterdamDataUpdateCoordinator, +) from .entity import GaragesAmsterdamEntity diff --git a/homeassistant/components/garages_amsterdam/coordinator.py b/homeassistant/components/garages_amsterdam/coordinator.py index 3d06aba79e2..74f2361980d 100644 --- a/homeassistant/components/garages_amsterdam/coordinator.py +++ b/homeassistant/components/garages_amsterdam/coordinator.py @@ -4,24 +4,31 @@ from __future__ import annotations from odp_amsterdam import Garage, ODPAmsterdam, VehicleType +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL +type GaragesAmsterdamConfigEntry = ConfigEntry[GaragesAmsterdamDataUpdateCoordinator] + class GaragesAmsterdamDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Garage]]): """Class to manage fetching Garages Amsterdam data from single endpoint.""" + config_entry: GaragesAmsterdamConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: GaragesAmsterdamConfigEntry, client: ODPAmsterdam, ) -> None: """Initialize global Garages Amsterdam data updater.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index b562fff841a..8c16260c58b 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -16,8 +16,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import GaragesAmsterdamConfigEntry -from .coordinator import GaragesAmsterdamDataUpdateCoordinator +from .coordinator import ( + GaragesAmsterdamConfigEntry, + GaragesAmsterdamDataUpdateCoordinator, +) from .entity import GaragesAmsterdamEntity diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 47034e61fb9..34f72bf0a5a 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -10,7 +10,6 @@ from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation from gardena_bluetooth.exceptions import CommunicationFailure from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -18,7 +17,11 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import DeviceUnavailable, GardenaBluetoothCoordinator +from .coordinator import ( + DeviceUnavailable, + GardenaBluetoothConfigEntry, + GardenaBluetoothCoordinator, +) PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -32,8 +35,6 @@ LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 DISCONNECT_DELAY = 5 -type GardenaBluetoothConfigEntry = ConfigEntry[GardenaBluetoothCoordinator] - def get_connection(hass: HomeAssistant, address: str) -> CachedConnection: """Set up a cached client that keeps connection after last use.""" @@ -80,7 +81,7 @@ async def async_setup_entry( ) coordinator = GardenaBluetoothCoordinator( - hass, LOGGER, client, uuids, device, address + hass, entry, LOGGER, client, uuids, device, address ) entry.runtime_data = coordinator diff --git a/homeassistant/components/gardena_bluetooth/binary_sensor.py b/homeassistant/components/gardena_bluetooth/binary_sensor.py index d3ae096e291..4ee3dd511e9 100644 --- a/homeassistant/components/gardena_bluetooth/binary_sensor.py +++ b/homeassistant/components/gardena_bluetooth/binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GardenaBluetoothConfigEntry +from .coordinator import GardenaBluetoothConfigEntry from .entity import GardenaBluetoothDescriptorEntity diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py index 9d87cba2446..8390baa5943 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -12,7 +12,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GardenaBluetoothConfigEntry +from .coordinator import GardenaBluetoothConfigEntry from .entity import GardenaBluetoothDescriptorEntity diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 5caafe0e794..f85fb839657 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -12,6 +12,7 @@ from gardena_bluetooth.exceptions import ( ) from gardena_bluetooth.parse import Characteristic, CharacteristicType +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -20,6 +21,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda SCAN_INTERVAL = timedelta(seconds=60) LOGGER = logging.getLogger(__name__) +type GardenaBluetoothConfigEntry = ConfigEntry[GardenaBluetoothCoordinator] + class DeviceUnavailable(HomeAssistantError): """Raised if device can't be found.""" @@ -28,9 +31,12 @@ class DeviceUnavailable(HomeAssistantError): class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]): """Class to manage fetching data.""" + config_entry: GardenaBluetoothConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: GardenaBluetoothConfigEntry, logger: logging.Logger, client: Client, characteristics: set[str], @@ -41,6 +47,7 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]): super().__init__( hass=hass, logger=logger, + config_entry=config_entry, name="Gardena Bluetooth Data Update Coordinator", update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index b55630fa797..eb95d9ff814 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -21,8 +21,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GardenaBluetoothConfigEntry -from .coordinator import GardenaBluetoothCoordinator +from .coordinator import GardenaBluetoothConfigEntry, GardenaBluetoothCoordinator from .entity import GardenaBluetoothDescriptorEntity, GardenaBluetoothEntity diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index c07d2ba6866..29d1a3155de 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -19,8 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from . import GardenaBluetoothConfigEntry -from .coordinator import GardenaBluetoothCoordinator +from .coordinator import GardenaBluetoothConfigEntry, GardenaBluetoothCoordinator from .entity import GardenaBluetoothDescriptorEntity, GardenaBluetoothEntity diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index f82c39025a5..73c4867d040 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -11,8 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GardenaBluetoothConfigEntry -from .coordinator import GardenaBluetoothCoordinator +from .coordinator import GardenaBluetoothConfigEntry, GardenaBluetoothCoordinator from .entity import GardenaBluetoothEntity diff --git a/homeassistant/components/gardena_bluetooth/valve.py b/homeassistant/components/gardena_bluetooth/valve.py index ae6bf56a7ff..e51e5aa22ca 100644 --- a/homeassistant/components/gardena_bluetooth/valve.py +++ b/homeassistant/components/gardena_bluetooth/valve.py @@ -10,8 +10,7 @@ from homeassistant.components.valve import ValveEntity, ValveEntityFeature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GardenaBluetoothConfigEntry -from .coordinator import GardenaBluetoothCoordinator +from .coordinator import GardenaBluetoothConfigEntry, GardenaBluetoothCoordinator from .entity import GardenaBluetoothEntity FALLBACK_WATERING_TIME_IN_SECONDS = 60 * 60 diff --git a/homeassistant/components/geocaching/coordinator.py b/homeassistant/components/geocaching/coordinator.py index 8f56cd9d846..41b59d049af 100644 --- a/homeassistant/components/geocaching/coordinator.py +++ b/homeassistant/components/geocaching/coordinator.py @@ -18,12 +18,13 @@ from .const import DOMAIN, ENVIRONMENT, LOGGER, UPDATE_INTERVAL class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): """Class to manage fetching Geocaching data from single endpoint.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, entry: ConfigEntry, session: OAuth2Session ) -> None: """Initialize global Geocaching data updater.""" self.session = session - self.entry = entry async def async_token_refresh() -> str: await session.async_ensure_token_valid() @@ -39,7 +40,13 @@ class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): token_refresh_method=async_token_refresh, ) - 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 _async_update_data(self) -> GeocachingStatus: try: diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index b5a0e9d5371..c76efbcf361 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -2,32 +2,21 @@ from __future__ import annotations -from dataclasses import dataclass import logging from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM -from homeassistant.config_entries import ConfigEntry from homeassistant.const import 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_STATION_ID, DOMAIN -from .coordinator import GiosDataUpdateCoordinator +from .coordinator import GiosConfigEntry, GiosData, GiosDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -type GiosConfigEntry = ConfigEntry[GiosData] - - -@dataclass -class GiosData: - """Data for GIOS integration.""" - - coordinator: GiosDataUpdateCoordinator - async def async_setup_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool: """Set up GIOS as config entry.""" @@ -48,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool websession = async_get_clientsession(hass) - coordinator = GiosDataUpdateCoordinator(hass, websession, station_id) + coordinator = GiosDataUpdateCoordinator(hass, entry, websession, station_id) await coordinator.async_config_entry_first_refresh() entry.runtime_data = GiosData(coordinator) diff --git a/homeassistant/components/gios/coordinator.py b/homeassistant/components/gios/coordinator.py index 17b4b89174f..be4b41ca6ee 100644 --- a/homeassistant/components/gios/coordinator.py +++ b/homeassistant/components/gios/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass import logging from aiohttp import ClientSession @@ -11,6 +12,7 @@ from gios import Gios from gios.exceptions import GiosError from gios.model import GiosSensors +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,17 +20,38 @@ from .const import API_TIMEOUT, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +type GiosConfigEntry = ConfigEntry[GiosData] + + +@dataclass +class GiosData: + """Data for GIOS integration.""" + + coordinator: GiosDataUpdateCoordinator + class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]): """Define an object to hold GIOS data.""" + config_entry: GiosConfigEntry + def __init__( - self, hass: HomeAssistant, session: ClientSession, station_id: int + self, + hass: HomeAssistant, + config_entry: GiosConfigEntry, + session: ClientSession, + station_id: int, ) -> None: """Class to manage fetching GIOS data API.""" self.gios = Gios(station_id, session) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) async def _async_update_data(self) -> GiosSensors: """Update data via library.""" diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index a94a95254de..7e938d5ac6b 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.core import HomeAssistant -from . import GiosConfigEntry +from .coordinator import GiosConfigEntry async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 69e198d34df..096ea838a41 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -23,7 +23,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GiosConfigEntry from .const import ( ATTR_AQI, ATTR_C6H6, @@ -38,7 +37,7 @@ from .const import ( MANUFACTURER, URL, ) -from .coordinator import GiosDataUpdateCoordinator +from .coordinator import GiosConfigEntry, GiosDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 74575e38e09..dea2acf4f1b 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from aiogithubapi import GitHubAPI -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -14,14 +13,11 @@ from homeassistant.helpers.aiohttp_client import ( ) from .const import CONF_REPOSITORIES, DOMAIN, LOGGER -from .coordinator import GitHubDataUpdateCoordinator +from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -type GithubConfigEntry = ConfigEntry[dict[str, GitHubDataUpdateCoordinator]] - - async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool: """Set up GitHub from a config entry.""" client = GitHubAPI( @@ -36,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bo for repository in repositories: coordinator = GitHubDataUpdateCoordinator( hass=hass, + config_entry=entry, client=client, repository=repository, ) @@ -57,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bo @callback def async_cleanup_device_registry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GithubConfigEntry, ) -> None: """Remove entries form device registry if we no longer track the repository.""" device_registry = dr.async_get(hass) @@ -92,6 +89,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> b return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> None: """Handle an options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index e73e02932e9..adeda1fd88a 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -13,6 +13,7 @@ from aiogithubapi import ( GitHubResponseModel, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -98,13 +99,18 @@ query ($owner: String!, $repository: String!) { } """ +type GithubConfigEntry = ConfigEntry[dict[str, GitHubDataUpdateCoordinator]] + class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Data update coordinator for the GitHub integration.""" + config_entry: GithubConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: GithubConfigEntry, client: GitHubAPI, repository: str, ) -> None: @@ -118,6 +124,7 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, LOGGER, + config_entry=config_entry, name=repository, update_interval=FALLBACK_UPDATE_INTERVAL, ) diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index 8d2d496a813..41fef9406a4 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -6,7 +6,6 @@ from typing import Any from aiogithubapi import GitHubAPI, GitHubException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import ( @@ -14,10 +13,12 @@ from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, ) +from .coordinator import GithubConfigEntry + async def async_get_config_entry_diagnostics( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GithubConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" data = {"options": {**config_entry.options}} diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 614ebe254c4..a7ecb4ec8da 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -18,9 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GithubConfigEntry from .const import DOMAIN -from .coordinator import GitHubDataUpdateCoordinator +from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 9d09e63606e..d7b645d9e11 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -10,7 +10,6 @@ from glances_api.exceptions import ( GlancesApiNoDataAvailable, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -29,15 +28,13 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers.httpx_client import get_async_client -from .coordinator import GlancesDataUpdateCoordinator +from .coordinator import GlancesConfigEntry, GlancesDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -type GlancesConfigEntry = ConfigEntry[GlancesDataUpdateCoordinator] - async def async_setup_entry( hass: HomeAssistant, config_entry: GlancesConfigEntry diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 8882b097ba9..28cf40aae6e 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -17,21 +17,25 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) +type GlancesConfigEntry = ConfigEntry[GlancesDataUpdateCoordinator] + class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Get the latest data from Glances api.""" - config_entry: ConfigEntry + config_entry: GlancesConfigEntry - def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: Glances) -> None: + def __init__( + self, hass: HomeAssistant, entry: GlancesConfigEntry, api: Glances + ) -> None: """Initialize the Glances data.""" self.hass = hass - self.config_entry = entry self.host: str = entry.data[CONF_HOST] self.api = api super().__init__( hass, _LOGGER, + config_entry=entry, name=f"{DOMAIN} - {self.host}", update_interval=DEFAULT_SCAN_INTERVAL, ) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 0741926296e..61d88b744bf 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -22,8 +22,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GlancesConfigEntry, GlancesDataUpdateCoordinator from .const import CPU_ICON, DOMAIN +from .coordinator import GlancesConfigEntry, GlancesDataUpdateCoordinator @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 6698d1efc99..4a34927a585 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoalZeroConfigEntry) -> except exceptions.ConnectError as ex: raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex - entry.runtime_data = GoalZeroDataUpdateCoordinator(hass, api) + entry.runtime_data = GoalZeroDataUpdateCoordinator(hass, entry, api) await entry.runtime_data.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/goalzero/coordinator.py b/homeassistant/components/goalzero/coordinator.py index 3c7cd967482..97a7f537759 100644 --- a/homeassistant/components/goalzero/coordinator.py +++ b/homeassistant/components/goalzero/coordinator.py @@ -18,11 +18,14 @@ class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator[None]): config_entry: GoalZeroConfigEntry - def __init__(self, hass: HomeAssistant, api: Yeti) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: GoalZeroConfigEntry, api: Yeti + ) -> None: """Initialize the coordinator.""" super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=30), ) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 52b1788c23e..8506414ca33 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -62,6 +62,7 @@ def get_data_update_coordinator( config_entry_data[DATA_UPDATE_COORDINATOR] = DeviceDataUpdateCoordinator( hass, + config_entry, _LOGGER, api, # Name of the data. For logging purposes. diff --git a/homeassistant/components/gogogate2/coordinator.py b/homeassistant/components/gogogate2/coordinator.py index 7c15e8b1c32..c2e7cc47b46 100644 --- a/homeassistant/components/gogogate2/coordinator.py +++ b/homeassistant/components/gogogate2/coordinator.py @@ -8,6 +8,7 @@ import logging from ismartgate import AbstractGateApi, GogoGate2InfoResponse, ISmartGateInfoResponse +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -18,9 +19,12 @@ class DeviceDataUpdateCoordinator( ): """Manages polling for state changes from the device.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, logger: logging.Logger, api: AbstractGateApi, *, @@ -33,10 +37,10 @@ class DeviceDataUpdateCoordinator( request_refresh_debouncer: Debouncer | None = None, ) -> None: """Initialize the data update coordinator.""" - DataUpdateCoordinator.__init__( - self, + super().__init__( hass, logger, + config_entry=config_entry, name=name, update_interval=update_interval, update_method=update_method, diff --git a/homeassistant/components/goodwe/coordinator.py b/homeassistant/components/goodwe/coordinator.py index a8ee7df6337..914ba3155b4 100644 --- a/homeassistant/components/goodwe/coordinator.py +++ b/homeassistant/components/goodwe/coordinator.py @@ -19,6 +19,8 @@ _LOGGER = logging.getLogger(__name__) class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Gather data for the energy device.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, @@ -29,6 +31,7 @@ class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, _LOGGER, + config_entry=entry, name=entry.title, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 5ac5dae616c..82208420b8c 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -266,6 +266,7 @@ async def async_setup_entry( if not entity_description.local_sync: coordinator = CalendarQueryUpdateCoordinator( hass, + config_entry, calendar_service, entity_description.name or entity_description.key, calendar_id, @@ -285,6 +286,7 @@ async def async_setup_entry( ) coordinator = CalendarSyncUpdateCoordinator( hass, + config_entry, sync, entity_description.name or entity_description.key, ) diff --git a/homeassistant/components/google/coordinator.py b/homeassistant/components/google/coordinator.py index 19198041c05..4a8a3d9f167 100644 --- a/homeassistant/components/google/coordinator.py +++ b/homeassistant/components/google/coordinator.py @@ -52,6 +52,7 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, sync: CalendarEventSyncManager, name: str, ) -> None: @@ -59,6 +60,7 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=name, update_interval=MIN_TIME_BETWEEN_UPDATES, ) @@ -111,6 +113,7 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, calendar_service: GoogleCalendarService, name: str, calendar_id: str, @@ -120,6 +123,7 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=name, update_interval=MIN_TIME_BETWEEN_UPDATES, ) diff --git a/homeassistant/components/google_drive/__init__.py b/homeassistant/components/google_drive/__init__.py index af93956931a..b30bc2ae1f6 100644 --- a/homeassistant/components/google_drive/__init__.py +++ b/homeassistant/components/google_drive/__init__.py @@ -7,7 +7,7 @@ from collections.abc import Callable from google_drive_api.exceptions import GoogleDriveApiError from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -49,6 +49,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleDriveConfigEntry) except GoogleDriveApiError as err: raise ConfigEntryNotReady from err + _async_notify_backup_listeners_soon(hass) + return True @@ -56,10 +58,15 @@ async def async_unload_entry( hass: HomeAssistant, entry: GoogleDriveConfigEntry ) -> bool: """Unload a config entry.""" - hass.loop.call_soon(_notify_backup_listeners, hass) + _async_notify_backup_listeners_soon(hass) return True -def _notify_backup_listeners(hass: HomeAssistant) -> None: +def _async_notify_backup_listeners(hass: HomeAssistant) -> None: for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []): listener() + + +@callback +def _async_notify_backup_listeners_soon(hass: HomeAssistant) -> None: + hass.loop.call_soon(_async_notify_backup_listeners, hass) 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/const.py b/homeassistant/components/google_generative_ai_conversation/const.py index bd60e8d94c1..4d83b935528 100644 --- a/homeassistant/components/google_generative_ai_conversation/const.py +++ b/homeassistant/components/google_generative_ai_conversation/const.py @@ -8,7 +8,7 @@ CONF_PROMPT = "prompt" CONF_RECOMMENDED = "recommended" CONF_CHAT_MODEL = "chat_model" -RECOMMENDED_CHAT_MODEL = "models/gemini-1.5-flash-latest" +RECOMMENDED_CHAT_MODEL = "models/gemini-2.0-flash" CONF_TEMPERATURE = "temperature" RECOMMENDED_TEMPERATURE = 1.0 CONF_TOP_P = "top_p" 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_photos/__init__.py b/homeassistant/components/google_photos/__init__.py index 2a7109d8189..40de02554ae 100644 --- a/homeassistant/components/google_photos/__init__.py +++ b/homeassistant/components/google_photos/__init__.py @@ -12,9 +12,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import api from .const import DOMAIN -from .coordinator import GooglePhotosUpdateCoordinator +from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator from .services import async_register_services -from .types import GooglePhotosConfigEntry __all__ = [ "DOMAIN", @@ -43,7 +42,9 @@ async def async_setup_entry( raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - coordinator = GooglePhotosUpdateCoordinator(hass, GooglePhotosLibraryApi(auth)) + coordinator = GooglePhotosUpdateCoordinator( + hass, entry, GooglePhotosLibraryApi(auth) + ) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/google_photos/coordinator.py b/homeassistant/components/google_photos/coordinator.py index 3ba5a8124d6..215d40d7864 100644 --- a/homeassistant/components/google_photos/coordinator.py +++ b/homeassistant/components/google_photos/coordinator.py @@ -15,6 +15,7 @@ from google_photos_library_api.api import GooglePhotosLibraryApi from google_photos_library_api.exceptions import GooglePhotosApiError from google_photos_library_api.model import Album, NewAlbum +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -23,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL: Final = datetime.timedelta(hours=24) ALBUM_PAGE_SIZE = 50 +type GooglePhotosConfigEntry = ConfigEntry[GooglePhotosUpdateCoordinator] + class GooglePhotosUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): """Coordinator for fetching Google Photos albums. @@ -30,11 +33,19 @@ class GooglePhotosUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): The `data` object is a dict from Album ID to Album title. """ - def __init__(self, hass: HomeAssistant, client: GooglePhotosLibraryApi) -> None: + config_entry: GooglePhotosConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: GooglePhotosConfigEntry, + client: GooglePhotosLibraryApi, + ) -> None: """Initialize TaskUpdateCoordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="Google Photos", update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/google_photos/media_source.py b/homeassistant/components/google_photos/media_source.py index 7ee81b51bc0..c0a87e46fbc 100644 --- a/homeassistant/components/google_photos/media_source.py +++ b/homeassistant/components/google_photos/media_source.py @@ -20,8 +20,8 @@ from homeassistant.components.media_source import ( ) from homeassistant.core import HomeAssistant -from . import GooglePhotosConfigEntry from .const import DOMAIN, READ_SCOPE +from .coordinator import GooglePhotosConfigEntry _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_photos/services.py b/homeassistant/components/google_photos/services.py index 22d3cc7deb0..8042df8f811 100644 --- a/homeassistant/components/google_photos/services.py +++ b/homeassistant/components/google_photos/services.py @@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv from .const import DOMAIN, UPLOAD_SCOPE -from .types import GooglePhotosConfigEntry +from .coordinator import GooglePhotosConfigEntry CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ALBUM = "album" diff --git a/homeassistant/components/google_photos/types.py b/homeassistant/components/google_photos/types.py deleted file mode 100644 index 4f4cc1845e4..00000000000 --- a/homeassistant/components/google_photos/types.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Google Photos types.""" - -from homeassistant.config_entries import ConfigEntry - -from .coordinator import GooglePhotosUpdateCoordinator - -type GooglePhotosConfigEntry = ConfigEntry[GooglePhotosUpdateCoordinator] 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/google_tasks/__init__.py b/homeassistant/components/google_tasks/__init__.py index 45ad1777aa0..2d570854ad4 100644 --- a/homeassistant/components/google_tasks/__init__.py +++ b/homeassistant/components/google_tasks/__init__.py @@ -13,9 +13,8 @@ from homeassistant.helpers import config_entry_oauth2_flow from . import api from .const import DOMAIN -from .coordinator import TaskUpdateCoordinator +from .coordinator import GoogleTasksConfigEntry, TaskUpdateCoordinator from .exceptions import GoogleTasksApiError -from .types import GoogleTasksConfigEntry __all__ = [ "DOMAIN", @@ -52,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) coordinators = [ TaskUpdateCoordinator( hass, + entry, auth, task_list["id"], task_list["title"], diff --git a/homeassistant/components/google_tasks/coordinator.py b/homeassistant/components/google_tasks/coordinator.py index a06faf00a91..b61fe1c30db 100644 --- a/homeassistant/components/google_tasks/coordinator.py +++ b/homeassistant/components/google_tasks/coordinator.py @@ -5,6 +5,7 @@ import datetime import logging from typing import Any, Final +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -15,13 +16,18 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL: Final = datetime.timedelta(minutes=30) TIMEOUT = 10 +type GoogleTasksConfigEntry = ConfigEntry[list[TaskUpdateCoordinator]] + class TaskUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): """Coordinator for fetching Google Tasks for a Task List form the API.""" + config_entry: GoogleTasksConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: GoogleTasksConfigEntry, api: AsyncConfigEntryAuth, task_list_id: str, task_list_title: str, @@ -30,6 +36,7 @@ class TaskUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"Google Tasks {task_list_id}", update_interval=UPDATE_INTERVAL, ) diff --git a/homeassistant/components/google_tasks/todo.py b/homeassistant/components/google_tasks/todo.py index 1df5e5fc2e9..6d1969d9a8a 100644 --- a/homeassistant/components/google_tasks/todo.py +++ b/homeassistant/components/google_tasks/todo.py @@ -16,8 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from .coordinator import TaskUpdateCoordinator -from .types import GoogleTasksConfigEntry +from .coordinator import GoogleTasksConfigEntry, TaskUpdateCoordinator PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/google_tasks/types.py b/homeassistant/components/google_tasks/types.py deleted file mode 100644 index 21500d11eb8..00000000000 --- a/homeassistant/components/google_tasks/types.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Types for the Google Tasks integration.""" - -from homeassistant.config_entries import ConfigEntry - -from .coordinator import TaskUpdateCoordinator - -type GoogleTasksConfigEntry = ConfigEntry[list[TaskUpdateCoordinator]] diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 4d871a991a6..1c61ae31010 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -38,6 +38,10 @@ "local_name": "GV5126*", "connectable": false }, + { + "local_name": "GV5179*", + "connectable": false + }, { "local_name": "GVH5127*", "connectable": false @@ -131,5 +135,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.42.1"] + "requirements": ["govee-ble==0.43.0"] } diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index 44dbc825665..ee04dd81088 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool: """Set up Govee light local from a config entry.""" - coordinator = GoveeLocalApiCoordinator(hass=hass) + coordinator = GoveeLocalApiCoordinator(hass, entry) async def await_cleanup(): cleanup_complete: asyncio.Event = coordinator.cleanup() diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 240313a34b8..ecbed0c4f65 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -26,11 +26,16 @@ type GoveeLocalConfigEntry = ConfigEntry[GoveeLocalApiCoordinator] class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): """Govee light local coordinator.""" - def __init__(self, hass: HomeAssistant) -> None: + config_entry: GoveeLocalConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: GoveeLocalConfigEntry + ) -> None: """Initialize my coordinator.""" super().__init__( hass=hass, logger=_LOGGER, + config_entry=config_entry, name="GoveeLightLocalApi", update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index e813ab545df..cba341cd482 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==2.0.0"] + "requirements": ["govee-local-api==2.0.1"] } diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 70d32f88a65..86d3ab7cc04 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -6,7 +6,6 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import logging -from typing import Any from gps3.agps3threaded import AGPS3mechanism @@ -38,7 +37,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_CLIMB = "climb" ATTR_ELEVATION = "elevation" -ATTR_GPS_TIME = "gps_time" ATTR_SPEED = "speed" ATTR_TOTAL_SATELLITES = "total_satellites" ATTR_USED_SATELLITES = "used_satellites" @@ -201,21 +199,3 @@ class GpsdSensor(SensorEntity): """Return the state of GPSD.""" value = self.entity_description.value_fn(self.agps_thread) return None if value == "n/a" else value - - # Deprecated since Home Assistant 2024.9.0 - # Can be removed completely in 2025.3.0 - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the GPS.""" - if self.entity_description.key != ATTR_MODE: - return None - - return { - ATTR_LATITUDE: self.agps_thread.data_stream.lat, - ATTR_LONGITUDE: self.agps_thread.data_stream.lon, - ATTR_ELEVATION: self.agps_thread.data_stream.alt, - ATTR_GPS_TIME: self.agps_thread.data_stream.time, - ATTR_SPEED: self.agps_thread.data_stream.speed, - ATTR_CLIMB: self.agps_thread.data_stream.climb, - ATTR_MODE: self.agps_thread.data_stream.mode, - } diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index c385ce45262..7cb4f0f0921 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -26,7 +26,7 @@ PLATFORMS = [Platform.CLIMATE, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Gree Climate from a config entry.""" hass.data.setdefault(DOMAIN, {}) - gree_discovery = DiscoveryService(hass) + gree_discovery = DiscoveryService(hass, entry) hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery async def _async_scan_update(_=None): diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 42d6734a6b2..0d1aa60deaa 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -11,6 +11,7 @@ from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError from greeclimate.network import Response +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.json import json_dumps @@ -32,12 +33,16 @@ _LOGGER = logging.getLogger(__name__) class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" - def __init__(self, hass: HomeAssistant, device: Device) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device + ) -> None: """Initialize the data update coordinator.""" - DataUpdateCoordinator.__init__( - self, + super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"{DOMAIN}-{device.device_info.name}", update_interval=timedelta(seconds=UPDATE_INTERVAL), always_update=False, @@ -117,10 +122,11 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): class DiscoveryService(Listener): """Discovery event handler for gree devices.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize discovery service.""" super().__init__() self.hass = hass + self.entry = entry self.discovery = Discovery(DISCOVERY_TIMEOUT) self.discovery.add_listener(self) @@ -144,7 +150,7 @@ class DiscoveryService(Listener): device.device_info.ip, device.device_info.port, ) - coordo = DeviceDataUpdateCoordinator(self.hass, device) + coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device) self.hass.data[DOMAIN][COORDINATORS].append(coordo) await coordo.async_refresh() diff --git a/homeassistant/components/guardian/coordinator.py b/homeassistant/components/guardian/coordinator.py index 849cec8063c..500b7c10784 100644 --- a/homeassistant/components/guardian/coordinator.py +++ b/homeassistant/components/guardian/coordinator.py @@ -42,6 +42,7 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, LOGGER, + config_entry=entry, name=f"{valve_controller_uid}_{api_name}", update_interval=DEFAULT_UPDATE_INTERVAL, ) @@ -50,7 +51,6 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._api_lock = api_lock self._client = client - self.config_entry = entry self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( self.config_entry.entry_id ) diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 6ace6d45509..9ea346a0dcb 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/habitica", "iot_class": "cloud_polling", "loggers": ["habiticalib"], - "requirements": ["habiticalib==0.3.4"] + "requirements": ["habiticalib==0.3.5"] } diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py index a28aada85fa..2537655dbfb 100644 --- a/homeassistant/components/habitica/services.py +++ b/homeassistant/components/habitica/services.py @@ -77,7 +77,7 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( SERVICE_CAST_SKILL_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), vol.Required(ATTR_SKILL): cv.string, vol.Optional(ATTR_TASK): cv.string, } @@ -85,12 +85,12 @@ SERVICE_CAST_SKILL_SCHEMA = vol.Schema( SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), } ) SERVICE_SCORE_TASK_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), vol.Required(ATTR_TASK): cv.string, vol.Optional(ATTR_DIRECTION): cv.string, } @@ -98,7 +98,7 @@ SERVICE_SCORE_TASK_SCHEMA = vol.Schema( SERVICE_TRANSFORMATION_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), vol.Required(ATTR_ITEM): cv.string, vol.Required(ATTR_TARGET): cv.string, } @@ -106,7 +106,7 @@ SERVICE_TRANSFORMATION_SCHEMA = vol.Schema( SERVICE_GET_TASKS_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), vol.Optional(ATTR_TYPE): vol.All( cv.ensure_list, [vol.All(vol.Upper, vol.In({x.name for x in TaskType}))] ), @@ -510,7 +510,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 or (task.notes and keyword in task.notes.lower()) or any(keyword in item.text.lower() for item in task.checklist) ] - result: dict[str, Any] = {"tasks": response} + result: dict[str, Any] = {"tasks": [task.to_dict() for task in response]} + return result hass.services.async_register( diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index d735469c5cb..7bbd3765602 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -37,11 +37,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool for device in device_registry.devices.get_devices_for_config_entry_id( entry.entry_id ): - 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))} + for ident in device.identifiers: + if ident[0] != DOMAIN or isinstance(ident[1], str): + continue + + player_id = int(ident[1]) # type: ignore[unreachable] + + # Create set of identifiers excluding this integration + identifiers = {ident for ident in device.identifiers if ident[0] != 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/heos/coordinator.py b/homeassistant/components/heos/coordinator.py index dc8989fd55b..94aa4ad0ab5 100644 --- a/homeassistant/components/heos/coordinator.py +++ b/homeassistant/components/heos/coordinator.py @@ -82,12 +82,20 @@ class HeosCoordinator(DataUpdateCoordinator[None]): try: await self.heos.connect() except HeosError as error: - raise ConfigEntryNotReady from error + _LOGGER.debug("Unable to connect to %s", self.host, exc_info=True) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="unable_to_connect", + translation_placeholders={"host": self.host}, + ) from error # Load players try: await self.heos.get_players() except HeosError as error: - raise ConfigEntryNotReady from error + _LOGGER.debug("Unexpected error retrieving players", exc_info=True) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, translation_key="unable_to_get_players" + ) from error if not self.heos.is_signed_in: _LOGGER.warning( diff --git a/homeassistant/components/heos/quality_scale.yaml b/homeassistant/components/heos/quality_scale.yaml index f5066d0a743..67022ec492c 100644 --- a/homeassistant/components/heos/quality_scale.yaml +++ b/homeassistant/components/heos/quality_scale.yaml @@ -54,7 +54,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: done reconfiguration-flow: done repair-issues: todo diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 4092d4360db..53e20a032b5 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -112,6 +112,12 @@ "not_heos_media_player": { "message": "Entity {entity_id} is not a HEOS media player entity" }, + "unable_to_connect": { + "message": "Unable to connect to {host}" + }, + "unable_to_get_players": { + "message": "Unexpected error retrieving players" + }, "unknown_source": { "message": "Unknown source: {source}" } diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index cfa14a3e3ca..c0534fa7154 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -5,11 +5,11 @@ "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "mode": "Travel Mode" + "mode": "Travel mode" } }, "origin_menu": { - "title": "Choose Origin", + "title": "Choose origin", "menu_options": { "origin_coordinates": "Using a map location", "origin_entity": "Using an entity" @@ -28,7 +28,7 @@ } }, "destination_menu": { - "title": "Choose Destination", + "title": "Choose destination", "menu_options": { "destination_coordinates": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_coordinates%]", "destination_entity": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_entity%]" @@ -60,13 +60,13 @@ "step": { "init": { "data": { - "traffic_mode": "Traffic Mode", - "route_mode": "Route Mode", + "traffic_mode": "Traffic mode", + "route_mode": "Route mode", "unit_system": "Unit system" } }, "time_menu": { - "title": "Choose Time Type", + "title": "Choose time type", "menu_options": { "departure_time": "Configure a departure time", "arrival_time": "Configure an arrival time", @@ -74,15 +74,15 @@ } }, "departure_time": { - "title": "Choose Departure Time", + "title": "Choose departure time", "data": { - "departure_time": "Departure Time" + "departure_time": "Departure time" } }, "arrival_time": { - "title": "Choose Arrival Time", + "title": "Choose arrival time", "data": { - "arrival_time": "Arrival Time" + "arrival_time": "Arrival time" } } } diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 41d359446fa..94085af2fc3 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -7,5 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], - "requirements": ["aiohomeconnect==0.12.3"] + "requirements": ["aiohomeconnect==0.12.3"], + "single_config_entry": true } diff --git a/homeassistant/components/homeassistant_hardware/__init__.py b/homeassistant/components/homeassistant_hardware/__init__.py index c33dabe1ec8..fc2b393805e 100644 --- a/homeassistant/components/homeassistant_hardware/__init__.py +++ b/homeassistant/components/homeassistant_hardware/__init__.py @@ -6,10 +6,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -DOMAIN = "homeassistant_hardware" +from .const import DATA_COMPONENT, DOMAIN +from .helpers import HardwareInfoDispatcher + CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the component.""" + + hass.data[DATA_COMPONENT] = HardwareInfoDispatcher(hass) + return True diff --git a/homeassistant/components/homeassistant_hardware/const.py b/homeassistant/components/homeassistant_hardware/const.py index 8fddbe41b7d..a3c091ff7ee 100644 --- a/homeassistant/components/homeassistant_hardware/const.py +++ b/homeassistant/components/homeassistant_hardware/const.py @@ -1,10 +1,23 @@ """Constants for the Homeassistant Hardware integration.""" +from __future__ import annotations + import logging +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .helpers import HardwareInfoDispatcher + LOGGER = logging.getLogger(__package__) +DOMAIN = "homeassistant_hardware" +DATA_COMPONENT: HassKey[HardwareInfoDispatcher] = HassKey(DOMAIN) + ZHA_DOMAIN = "zha" +OTBR_DOMAIN = "otbr" OTBR_ADDON_NAME = "OpenThread Border Router" OTBR_ADDON_MANAGER_DATA = "openthread_border_router" diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py index fac3d2d9735..8d7a302e786 100644 --- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -25,12 +25,14 @@ from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.hassio import is_hassio from . import silabs_multiprotocol_addon -from .const import ZHA_DOMAIN +from .const import OTBR_DOMAIN, ZHA_DOMAIN from .util import ( ApplicationType, + OwningAddon, + OwningIntegration, get_otbr_addon_manager, - get_zha_device_path, get_zigbee_flasher_addon_manager, + guess_hardware_owners, probe_silabs_firmware_type, ) @@ -519,19 +521,15 @@ class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow): ) -> ConfigFlowResult: """Pick Zigbee firmware.""" assert self._device is not None + owners = await guess_hardware_owners(self.hass, self._device) - if is_hassio(self.hass): - otbr_manager = get_otbr_addon_manager(self.hass) - otbr_addon_info = await self._async_get_addon_info(otbr_manager) - - if ( - otbr_addon_info.state != AddonState.NOT_INSTALLED - and otbr_addon_info.options.get("device") == self._device - ): - raise AbortFlow( - "otbr_still_using_stick", - description_placeholders=self._get_translation_placeholders(), - ) + for info in owners: + for owner in info.owners: + if info.source == OTBR_DOMAIN and isinstance(owner, OwningAddon): + raise AbortFlow( + "otbr_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) return await super().async_step_pick_firmware_zigbee(user_input) @@ -541,15 +539,14 @@ class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow): """Pick Thread firmware.""" assert self._device is not None - for zha_entry in self.hass.config_entries.async_entries( - ZHA_DOMAIN, - include_ignore=False, - include_disabled=True, - ): - if get_zha_device_path(zha_entry) == self._device: - raise AbortFlow( - "zha_still_using_stick", - description_placeholders=self._get_translation_placeholders(), - ) + owners = await guess_hardware_owners(self.hass, self._device) + + for info in owners: + for owner in info.owners: + if info.source == ZHA_DOMAIN and isinstance(owner, OwningIntegration): + raise AbortFlow( + "zha_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) return await super().async_step_pick_firmware_thread(user_input) diff --git a/homeassistant/components/homeassistant_hardware/helpers.py b/homeassistant/components/homeassistant_hardware/helpers.py new file mode 100644 index 00000000000..a9b3703ee4a --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/helpers.py @@ -0,0 +1,143 @@ +"""Home Assistant Hardware integration helpers.""" + +from collections import defaultdict +from collections.abc import AsyncIterator, Awaitable, Callable +import logging +from typing import Protocol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback + +from . import DATA_COMPONENT +from .util import FirmwareInfo + +_LOGGER = logging.getLogger(__name__) + + +class SyncHardwareFirmwareInfoModule(Protocol): + """Protocol type for Home Assistant Hardware firmware info platform modules.""" + + def get_firmware_info( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> FirmwareInfo | None: + """Return radio firmware information for the config entry, synchronously.""" + + +class AsyncHardwareFirmwareInfoModule(Protocol): + """Protocol type for Home Assistant Hardware firmware info platform modules.""" + + async def async_get_firmware_info( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> FirmwareInfo | None: + """Return radio firmware information for the config entry, asynchronously.""" + + +type HardwareFirmwareInfoModule = ( + SyncHardwareFirmwareInfoModule | AsyncHardwareFirmwareInfoModule +) + + +class HardwareInfoDispatcher: + """Central dispatcher for hardware/firmware information.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the dispatcher.""" + self.hass = hass + self._providers: dict[str, HardwareFirmwareInfoModule] = {} + self._notification_callbacks: defaultdict[ + str, set[Callable[[FirmwareInfo], None]] + ] = defaultdict(set) + + def register_firmware_info_provider( + self, domain: str, platform: HardwareFirmwareInfoModule + ) -> None: + """Register a firmware info provider.""" + if domain in self._providers: + raise ValueError( + f"Domain {domain} is already registered as a firmware info provider" + ) + + # There is no need to handle "unregistration" because integrations cannot be + # wholly removed at runtime + self._providers[domain] = platform + _LOGGER.debug( + "Registered firmware info provider from domain %r: %s", domain, platform + ) + + def register_firmware_info_callback( + self, device: str, callback: Callable[[FirmwareInfo], None] + ) -> CALLBACK_TYPE: + """Register a firmware info notification callback.""" + self._notification_callbacks[device].add(callback) + + @hass_callback + def async_remove_callback() -> None: + self._notification_callbacks[device].discard(callback) + + return async_remove_callback + + async def notify_firmware_info( + self, domain: str, firmware_info: FirmwareInfo + ) -> None: + """Notify the dispatcher of new firmware information.""" + _LOGGER.debug( + "Received firmware info notification from %r: %s", domain, firmware_info + ) + + for callback in self._notification_callbacks.get(firmware_info.device, []): + try: + callback(firmware_info) + except Exception: + _LOGGER.exception( + "Error while notifying firmware info listener %s", callback + ) + + async def iter_firmware_info(self) -> AsyncIterator[FirmwareInfo]: + """Iterate over all firmware information for all hardware.""" + for domain, fw_info_module in self._providers.items(): + for config_entry in self.hass.config_entries.async_entries(domain): + try: + if hasattr(fw_info_module, "get_firmware_info"): + fw_info = fw_info_module.get_firmware_info( + self.hass, config_entry + ) + else: + fw_info = await fw_info_module.async_get_firmware_info( + self.hass, config_entry + ) + except Exception: + _LOGGER.exception( + "Error while getting firmware info from %r", fw_info_module + ) + continue + + if fw_info is not None: + yield fw_info + + +@hass_callback +def async_register_firmware_info_provider( + hass: HomeAssistant, domain: str, platform: HardwareFirmwareInfoModule +) -> None: + """Register a firmware info provider.""" + return hass.data[DATA_COMPONENT].register_firmware_info_provider(domain, platform) + + +@hass_callback +def async_register_firmware_info_callback( + hass: HomeAssistant, device: str, callback: Callable[[FirmwareInfo], None] +) -> CALLBACK_TYPE: + """Register a firmware info provider.""" + return hass.data[DATA_COMPONENT].register_firmware_info_callback(device, callback) + + +@hass_callback +def async_notify_firmware_info( + hass: HomeAssistant, domain: str, firmware_info: FirmwareInfo +) -> Awaitable[None]: + """Notify the dispatcher of new firmware information.""" + return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info) 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/homeassistant_hardware/util.py b/homeassistant/components/homeassistant_hardware/util.py index 3fd5bc60037..53cbcbae5d4 100644 --- a/homeassistant/components/homeassistant_hardware/util.py +++ b/homeassistant/components/homeassistant_hardware/util.py @@ -2,27 +2,27 @@ from __future__ import annotations +import asyncio from collections import defaultdict from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum import logging -from typing import cast from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType from universal_silabs_flasher.flasher import Flasher from homeassistant.components.hassio import AddonError, AddonState -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.singleton import singleton +from . import DATA_COMPONENT from .const import ( OTBR_ADDON_MANAGER_DATA, OTBR_ADDON_NAME, OTBR_ADDON_SLUG, - ZHA_DOMAIN, ZIGBEE_FLASHER_ADDON_MANAGER_DATA, ZIGBEE_FLASHER_ADDON_NAME, ZIGBEE_FLASHER_ADDON_SLUG, @@ -55,11 +55,6 @@ class ApplicationType(StrEnum): return FlasherApplicationType(self.value) -def get_zha_device_path(config_entry: ConfigEntry) -> str | None: - """Get the device path from a ZHA config entry.""" - return cast(str | None, config_entry.data.get("device", {}).get("path", None)) - - @singleton(OTBR_ADDON_MANAGER_DATA) @callback def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: @@ -84,31 +79,80 @@ def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager ) -@dataclass(slots=True, kw_only=True) -class FirmwareGuess: +@dataclass(kw_only=True) +class OwningAddon: + """Owning add-on.""" + + slug: str + + def _get_addon_manager(self, hass: HomeAssistant) -> WaitingAddonManager: + return WaitingAddonManager( + hass, + _LOGGER, + f"Add-on {self.slug}", + self.slug, + ) + + async def is_running(self, hass: HomeAssistant) -> bool: + """Check if the add-on is running.""" + addon_manager = self._get_addon_manager(hass) + + try: + addon_info = await addon_manager.async_get_addon_info() + except AddonError: + return False + else: + return addon_info.state == AddonState.RUNNING + + +@dataclass(kw_only=True) +class OwningIntegration: + """Owning integration.""" + + config_entry_id: str + + async def is_running(self, hass: HomeAssistant) -> bool: + """Check if the integration is running.""" + if (entry := hass.config_entries.async_get_entry(self.config_entry_id)) is None: + return False + + return entry.state in ( + ConfigEntryState.LOADED, + ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_IN_PROGRESS, + ) + + +@dataclass(kw_only=True) +class FirmwareInfo: """Firmware guess.""" - is_running: bool + device: str firmware_type: ApplicationType + firmware_version: str | None + source: str + owners: list[OwningAddon | OwningIntegration] + + async def is_running(self, hass: HomeAssistant) -> bool: + """Check if the firmware owner is running.""" + states = await asyncio.gather(*(o.is_running(hass) for o in self.owners)) + if not states: + return False + + return all(states) -async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess: - """Guess the firmware type based on installed addons and other integrations.""" - device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list) +async def guess_hardware_owners( + hass: HomeAssistant, device_path: str +) -> list[FirmwareInfo]: + """Guess the firmware info based on installed addons and other integrations.""" + device_guesses: defaultdict[str, list[FirmwareInfo]] = defaultdict(list) - for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): - zha_path = get_zha_device_path(zha_config_entry) - - if zha_path is not None: - device_guesses[zha_path].append( - FirmwareGuess( - is_running=(zha_config_entry.state == ConfigEntryState.LOADED), - firmware_type=ApplicationType.EZSP, - source="zha", - ) - ) + async for firmware_info in hass.data[DATA_COMPONENT].iter_firmware_info(): + device_guesses[firmware_info.device].append(firmware_info) + # It may be possible for the OTBR addon to be present without the integration if is_hassio(hass): otbr_addon_manager = get_otbr_addon_manager(hass) @@ -119,14 +163,22 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware else: if otbr_addon_info.state != AddonState.NOT_INSTALLED: otbr_path = otbr_addon_info.options.get("device") - device_guesses[otbr_path].append( - FirmwareGuess( - is_running=(otbr_addon_info.state == AddonState.RUNNING), - firmware_type=ApplicationType.SPINEL, - source="otbr", - ) - ) + # Only create a new entry if there are no existing OTBR ones + if otbr_path is not None and not any( + info.source == "otbr" for info in device_guesses[otbr_path] + ): + device_guesses[otbr_path].append( + FirmwareInfo( + device=otbr_path, + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[OwningAddon(slug=otbr_addon_manager.addon_slug)], + ) + ) + + if is_hassio(hass): multipan_addon_manager = await get_multiprotocol_addon_manager(hass) try: @@ -136,30 +188,48 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware else: if multipan_addon_info.state != AddonState.NOT_INSTALLED: multipan_path = multipan_addon_info.options.get("device") - device_guesses[multipan_path].append( - FirmwareGuess( - is_running=(multipan_addon_info.state == AddonState.RUNNING), - firmware_type=ApplicationType.CPC, - source="multiprotocol", - ) - ) - # Fall back to EZSP if we can't guess the firmware type - if device_path not in device_guesses: - return FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + if multipan_path is not None: + device_guesses[multipan_path].append( + FirmwareInfo( + device=multipan_path, + firmware_type=ApplicationType.CPC, + firmware_version=None, + source="multiprotocol", + owners=[ + OwningAddon(slug=multipan_addon_manager.addon_slug) + ], + ) + ) + + return device_guesses.get(device_path, []) + + +async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> FirmwareInfo: + """Guess the firmware type based on installed addons and other integrations.""" + + hardware_owners = await guess_hardware_owners(hass, device_path) + + # Fall back to EZSP if we have no way to guess + if not hardware_owners: + return FirmwareInfo( + device=device_path, + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="unknown", + owners=[], ) - # Prioritizes guesses that were pulled from a running addon or integration but keep - # the sort order we defined above - guesses = sorted( - device_guesses[device_path], - key=lambda guess: guess.is_running, - ) - + # Prioritize guesses that are pulled from a real source + guesses = [ + (guess, sum([await owner.is_running(hass) for owner in guess.owners])) + for guess in hardware_owners + ] + guesses.sort(key=lambda p: p[1]) assert guesses - return guesses[-1] + # Pick the best one. We use a stable sort so ZHA < OTBR < multi-PAN + return guesses[-1][0] async def probe_silabs_firmware_type( diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 43d42e4fa59..758f0c1e1ef 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from homeassistant.components.homeassistant_hardware.util import guess_firmware_type +from homeassistant.components.homeassistant_hardware.util import guess_firmware_info from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # Add-on startup with type service get started before Core, always (e.g. the # Multi-Protocol add-on). Probing the firmware would interfere with the add-on, # so we can't safely probe here. Instead, we must make an educated guess! - firmware_guess = await guess_firmware_type( + firmware_guess = await guess_firmware_info( hass, config_entry.data["device"] ) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index dc34cc4cdc9..b0837eeedbe 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon ) from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, - guess_firmware_type, + guess_firmware_info, ) from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry from homeassistant.core import HomeAssistant @@ -75,7 +75,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # Add-on startup with type service get started before Core, always (e.g. the # Multi-Protocol add-on). Probing the firmware would interfere with the add-on, # so we can't safely probe here. Instead, we must make an educated guess! - firmware_guess = await guess_firmware_type(hass, RADIO_DEVICE) + firmware_guess = await guess_firmware_info(hass, RADIO_DEVICE) new_data = {**config_entry.data} new_data[FIRMWARE] = firmware_guess.firmware_type.value diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 92b836d5ec6..dcdf6892dc2 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -2,13 +2,13 @@ "options": { "step": { "yaml": { - "title": "Adjust HomeKit Options", + "title": "Adjust HomeKit options", "description": "This entry is controlled via YAML" }, "init": { "data": { - "mode": "HomeKit Mode", - "include_exclude_mode": "Inclusion Mode", + "mode": "HomeKit mode", + "include_exclude_mode": "Inclusion mode", "domains": "[%key:component::homekit::config::step::user::data::include_domains%]" }, "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV or RECEIVER device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", @@ -40,14 +40,14 @@ "camera_audio": "Cameras that support audio" }, "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", - "title": "Camera Configuration" + "title": "Camera configuration" }, "advanced": { "data": { "devices": "Devices (Triggers)" }, "description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.", - "title": "Advanced Configuration" + "title": "Advanced configuration" } } }, @@ -72,7 +72,7 @@ "services": { "reload": { "name": "[%key:common::action::reload%]", - "description": "Reloads homekit and re-process YAML-configuration." + "description": "Reloads HomeKit and re-processes the YAML-configuration." }, "reset_accessory": { "name": "Reset accessory", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 582c65f2838..f6f5588956c 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -19,6 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_VIA_DEVICE, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, EntityCategory, UnitOfApparentPower, UnitOfElectricCurrent, @@ -137,6 +138,21 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( else None ), ), + HomeWizardSensorEntityDescription( + key="wifi_rssi", + translation_key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_fn=( + lambda data: data.system is not None + and data.system.wifi_rssi_db is not None + ), + value_fn=( + lambda data: data.system.wifi_rssi_db if data.system is not None else None + ), + ), HomeWizardSensorEntityDescription( key="total_power_import_kwh", translation_key="total_energy_import_kwh", diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json index 02b18d5fa4e..076e9375d24 100644 --- a/homeassistant/components/homewizard/strings.json +++ b/homeassistant/components/homewizard/strings.json @@ -78,6 +78,9 @@ "wifi_strength": { "name": "Wi-Fi strength" }, + "wifi_rssi": { + "name": "Wi-Fi RSSI" + }, "total_energy_import_kwh": { "name": "Energy import" }, diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index de45eb061d5..73423882e4a 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2025.1.0"] + "requirements": ["pydrawise==2025.2.0"] } diff --git a/homeassistant/components/keba/strings.json b/homeassistant/components/keba/strings.json index ed8594a1068..49ce01f4332 100644 --- a/homeassistant/components/keba/strings.json +++ b/homeassistant/components/keba/strings.json @@ -2,7 +2,7 @@ "services": { "request_data": { "name": "Request data", - "description": "Requesta new data from the charging station." + "description": "Requests new data from the charging station." }, "authorize": { "name": "Authorize", @@ -46,7 +46,7 @@ "fields": { "failsafe_timeout": { "name": "Failsafe timeout", - "description": "Timeout after which the failsafe mode is triggered, if set_current was not executed during this time." + "description": "Timeout after which the failsafe mode is triggered if the 'Set current' action was not run during this time." }, "failsafe_fallback": { "name": "Failsafe fallback", @@ -54,7 +54,7 @@ }, "failsafe_persist": { "name": "Failsafe persist", - "description": "If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot." + "description": "If set to 0, the failsafe option will be disabled after a charging station reboot. If set to 1, the failsafe option will survive a reboot." } } } diff --git a/homeassistant/components/keenetic_ndms2/strings.json b/homeassistant/components/keenetic_ndms2/strings.json index 765a3fc4d47..739846de0a8 100644 --- a/homeassistant/components/keenetic_ndms2/strings.json +++ b/homeassistant/components/keenetic_ndms2/strings.json @@ -21,7 +21,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "no_udn": "SSDP discovery info has no UDN", - "not_keenetic_ndms2": "Discovered item is not a Keenetic router" + "not_keenetic_ndms2": "Discovered device is not a Keenetic router" } }, "options": { @@ -33,7 +33,7 @@ "interfaces": "Choose interfaces to scan", "try_hotspot": "Use 'ip hotspot' data (most accurate)", "include_arp": "Use ARP data (ignored if hotspot data used)", - "include_associated": "Use WiFi AP associations data (ignored if hotspot data used)" + "include_associated": "Use Wi-Fi AP associations data (ignored if hotspot data used)" } } } 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/icons.json b/homeassistant/components/lg_thinq/icons.json index 87cf04e0c1a..42ae5746f24 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -7,8 +7,11 @@ "express_mode": { "default": "mdi:snowflake-variant" }, + "express_fridge": { + "default": "mdi:snowflake" + }, "hot_water_mode": { - "default": "mdi:list-status" + "default": "mdi:heat-wave" }, "humidity_warm_mode": { "default": "mdi:heat-wave" @@ -39,6 +42,9 @@ }, "warm_mode": { "default": "mdi:heat-wave" + }, + "display_light": { + "default": "mdi:lightbulb-on-outline" } }, "binary_sensor": { diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index a776dde2054..dee2d21e05a 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -30,10 +30,13 @@ "name": "Auto mode" }, "express_mode": { - "name": "Ice plus" + "name": "Express mode" + }, + "express_fridge": { + "name": "Express cool" }, "hot_water_mode": { - "name": "Hot water" + "name": "Heating water" }, "humidity_warm_mode": { "name": "Warm mist" @@ -64,6 +67,9 @@ }, "warm_mode": { "name": "Heating" + }, + "display_light": { + "name": "Lighting" } }, "binary_sensor": { @@ -209,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", @@ -426,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/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py index 25fd7eb8b64..6d69ce9a314 100644 --- a/homeassistant/components/lg_thinq/switch.py +++ b/homeassistant/components/lg_thinq/switch.py @@ -33,6 +33,18 @@ class ThinQSwitchEntityDescription(SwitchEntityDescription): DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ...]] = { DeviceType.AIR_CONDITIONER: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.AIR_CON_OPERATION_MODE, + translation_key="operation_power", + entity_category=EntityCategory.CONFIG, + ), + ThinQSwitchEntityDescription( + key=ThinQProperty.DISPLAY_LIGHT, + translation_key=ThinQProperty.DISPLAY_LIGHT, + on_key="on", + off_key="off", + entity_category=EntityCategory.CONFIG, + ), ThinQSwitchEntityDescription( key=ThinQProperty.POWER_SAVE_ENABLED, translation_key=ThinQProperty.POWER_SAVE_ENABLED, @@ -121,8 +133,20 @@ DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ... off_key="false", entity_category=EntityCategory.CONFIG, ), + ThinQSwitchEntityDescription( + key=ThinQProperty.EXPRESS_FRIDGE, + translation_key=ThinQProperty.EXPRESS_FRIDGE, + on_key="true", + off_key="false", + entity_category=EntityCategory.CONFIG, + ), ), DeviceType.SYSTEM_BOILER: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.BOILER_OPERATION_MODE, + translation_key="operation_power", + entity_category=EntityCategory.CONFIG, + ), ThinQSwitchEntityDescription( key=ThinQProperty.HOT_WATER_MODE, translation_key=ThinQProperty.HOT_WATER_MODE, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d87dcf41161..637ba45c7d9 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -8,7 +8,7 @@ import dataclasses from functools import partial import logging import os -from typing import Any, Final, Self, cast, final +from typing import TYPE_CHECKING, Any, Final, Self, cast, final from propcache.api import cached_property import voluptuous as vol @@ -528,6 +528,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes: rgb_color = params.pop(ATTR_RGB_COLOR) assert rgb_color is not None + if TYPE_CHECKING: + rgb_color = cast(tuple[int, int, int], rgb_color) if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: @@ -601,6 +603,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): rgbww_color = params.pop(ATTR_RGBWW_COLOR) assert rgbww_color is not None + if TYPE_CHECKING: + rgbww_color = cast(tuple[int, int, int, int, int], rgbww_color) rgb_color = color_util.color_rgbww_to_rgb( *rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) diff --git a/homeassistant/components/linear_garage_door/__init__.py b/homeassistant/components/linear_garage_door/__init__.py index 5d987a24b2a..e4aa30c98df 100644 --- a/homeassistant/components/linear_garage_door/__init__.py +++ b/homeassistant/components/linear_garage_door/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from .const import DOMAIN from .coordinator import LinearUpdateCoordinator @@ -15,6 +16,21 @@ PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Linear Garage Door from a config entry.""" + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + breaks_in_ha_version="2025.8.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_integration", + translation_placeholders={ + "nice_go": "https://www.home-assistant.io/integrations/linear_garage_door", + "entries": "/config/integrations/integration/linear_garage_door", + }, + ) + coordinator = LinearUpdateCoordinator(hass) await coordinator.async_config_entry_first_refresh() @@ -27,6 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py index 35ccced3274..38b1306ec38 100644 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ b/homeassistant/components/linear_garage_door/coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any +from typing import Any, cast from linear_garage_door import Linear from linear_garage_door.errors import InvalidLoginError @@ -56,7 +56,7 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, LinearDevice]]): for device in self._devices: device_id = str(device["id"]) state = await linear.get_device_state(device_id) - data[device_id] = LinearDevice(device["name"], state) + data[device_id] = LinearDevice(cast(str, device["name"]), state) return data return await self.execute(update_data) diff --git a/homeassistant/components/linear_garage_door/strings.json b/homeassistant/components/linear_garage_door/strings.json index 23624b4acfd..40ffcf22e8d 100644 --- a/homeassistant/components/linear_garage_door/strings.json +++ b/homeassistant/components/linear_garage_door/strings.json @@ -23,5 +23,11 @@ "name": "[%key:component::light::title%]" } } + }, + "issues": { + "deprecated_integration": { + "title": "The Linear Garage Door integration will be removed", + "description": "The Linear Garage Door integration will be removed as it has been replaced by the [Nice G.O.]({nice_go}) integration. Please migrate to the new integration.\n\nTo resolve this issue, please remove all Linear Garage Door entries from your configuration and add the new Nice G.O. integration. [Click here to see your existing Linear Garage Door integration entries]({entries})." + } } } diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 79b792935a8..0b432f88045 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -277,20 +277,6 @@ FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( } ) -PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP = { - "button_0": 2, - "button_2": 4, -} -PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LEAP = { - "button_0": 0, - "button_2": 2, -} -PADDLE_SWITCH_PICO_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( - { - vol.Required(CONF_SUBTYPE): vol.In(PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP), - } -) - DEVICE_TYPE_SCHEMA_MAP = { "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, @@ -302,7 +288,6 @@ DEVICE_TYPE_SCHEMA_MAP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, - "PaddleSwitchPico": PADDLE_SWITCH_PICO_TRIGGER_SCHEMA, } DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { @@ -315,7 +300,6 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP, "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP, - "PaddleSwitchPico": PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP, } DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { @@ -328,7 +312,6 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP, "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP, - "PaddleSwitchPico": PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LEAP, } LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP: dict[str, dict[int, str]] = { @@ -343,7 +326,6 @@ TRIGGER_SCHEMA = vol.Any( PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, - PADDLE_SWITCH_PICO_TRIGGER_SCHEMA, ) diff --git a/homeassistant/components/madvr/__init__.py b/homeassistant/components/madvr/__init__.py index bb42adb21fc..cf681bd0b65 100644 --- a/homeassistant/components/madvr/__init__.py +++ b/homeassistant/components/madvr/__init__.py @@ -6,17 +6,13 @@ import logging from madvr.madvr import Madvr -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant -from .coordinator import MadVRCoordinator +from .coordinator import MadVRConfigEntry, MadVRCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SENSOR] - -type MadVRConfigEntry = ConfigEntry[MadVRCoordinator] - _LOGGER = logging.getLogger(__name__) @@ -41,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MadVRConfigEntry) -> boo connect_timeout=10, loop=hass.loop, ) - coordinator = MadVRCoordinator(hass, madVRClient) + coordinator = MadVRCoordinator(hass, entry, madVRClient) entry.runtime_data = coordinator diff --git a/homeassistant/components/madvr/binary_sensor.py b/homeassistant/components/madvr/binary_sensor.py index 6a31f9cdcda..b6820f94fea 100644 --- a/homeassistant/components/madvr/binary_sensor.py +++ b/homeassistant/components/madvr/binary_sensor.py @@ -12,8 +12,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MadVRConfigEntry -from .coordinator import MadVRCoordinator +from .coordinator import MadVRConfigEntry, MadVRCoordinator from .entity import MadVREntity _HDR_FLAG = "hdr_flag" diff --git a/homeassistant/components/madvr/coordinator.py b/homeassistant/components/madvr/coordinator.py index 4031ba127f7..c1ed87fbee7 100644 --- a/homeassistant/components/madvr/coordinator.py +++ b/homeassistant/components/madvr/coordinator.py @@ -3,10 +3,11 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any from madvr.madvr import Madvr +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -14,8 +15,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -if TYPE_CHECKING: - from . import MadVRConfigEntry +type MadVRConfigEntry = ConfigEntry[MadVRCoordinator] class MadVRCoordinator(DataUpdateCoordinator[dict[str, Any]]): @@ -26,10 +26,11 @@ class MadVRCoordinator(DataUpdateCoordinator[dict[str, Any]]): def __init__( self, hass: HomeAssistant, + config_entry: MadVRConfigEntry, client: Madvr, ) -> None: """Initialize madvr coordinator.""" - super().__init__(hass, _LOGGER, name=DOMAIN) + super().__init__(hass, _LOGGER, config_entry=config_entry, name=DOMAIN) assert self.config_entry.unique_id self.mac = self.config_entry.unique_id self.client = client diff --git a/homeassistant/components/madvr/diagnostics.py b/homeassistant/components/madvr/diagnostics.py index f6261d27305..39e17a13d6f 100644 --- a/homeassistant/components/madvr/diagnostics.py +++ b/homeassistant/components/madvr/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from . import MadVRConfigEntry +from .coordinator import MadVRConfigEntry TO_REDACT = [CONF_HOST] diff --git a/homeassistant/components/madvr/remote.py b/homeassistant/components/madvr/remote.py index 4fe02b7ae47..032a1d718f5 100644 --- a/homeassistant/components/madvr/remote.py +++ b/homeassistant/components/madvr/remote.py @@ -10,8 +10,7 @@ from homeassistant.components.remote import RemoteEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MadVRConfigEntry -from .coordinator import MadVRCoordinator +from .coordinator import MadVRConfigEntry, MadVRCoordinator from .entity import MadVREntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/madvr/sensor.py b/homeassistant/components/madvr/sensor.py index 047b8bb83e6..e54e9dca476 100644 --- a/homeassistant/components/madvr/sensor.py +++ b/homeassistant/components/madvr/sensor.py @@ -16,7 +16,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import MadVRConfigEntry from .const import ( ASPECT_DEC, ASPECT_INT, @@ -45,7 +44,7 @@ from .const import ( TEMP_HDMI, TEMP_MAINBOARD, ) -from .coordinator import MadVRCoordinator +from .coordinator import MadVRConfigEntry, MadVRCoordinator from .entity import MadVREntity diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 6882078a712..484ed94fb90 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING, cast from chip.clusters import Objects as clusters from chip.clusters.Objects import uint @@ -55,6 +56,8 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity): value = None elif value_convert := self.entity_description.measurement_to_ha: value = value_convert(value) + if TYPE_CHECKING: + value = cast(bool | None, value) self._attr_is_on = value 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/mill/climate.py b/homeassistant/components/mill/climate.py index 0df2fe9335e..3cd9247c63a 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -105,10 +105,8 @@ class MillHeater(MillBaseEntity, ClimateEntity): self, coordinator: MillDataUpdateCoordinator, device: mill.Heater ) -> None: """Initialize the thermostat.""" - - super().__init__(coordinator, device) self._attr_unique_id = device.device_id - self._update_attr(device) + super().__init__(coordinator, device) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" diff --git a/homeassistant/components/mill/entity.py b/homeassistant/components/mill/entity.py index f24dbeb2c26..06056aba336 100644 --- a/homeassistant/components/mill/entity.py +++ b/homeassistant/components/mill/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod -from mill import Heater, MillDevice +from mill import MillDevice from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo @@ -45,7 +45,7 @@ class MillBaseEntity(CoordinatorEntity[MillDataUpdateCoordinator]): @abstractmethod @callback - def _update_attr(self, device: MillDevice | Heater) -> None: + def _update_attr(self, device: MillDevice) -> None: """Update the attribute of the entity.""" @property diff --git a/homeassistant/components/mill/number.py b/homeassistant/components/mill/number.py index af27159caf0..b4ef7bdd2c2 100644 --- a/homeassistant/components/mill/number.py +++ b/homeassistant/components/mill/number.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mill import MillDevice +from mill import Heater, MillDevice from homeassistant.components.number import NumberDeviceClass, NumberEntity from homeassistant.config_entries import ConfigEntry @@ -27,6 +27,7 @@ async def async_setup_entry( async_add_entities( MillNumber(mill_data_coordinator, mill_device) for mill_device in mill_data_coordinator.data.values() + if isinstance(mill_device, Heater) ) @@ -45,9 +46,8 @@ class MillNumber(MillBaseEntity, NumberEntity): mill_device: MillDevice, ) -> None: """Initialize the number.""" - super().__init__(coordinator, mill_device) self._attr_unique_id = f"{mill_device.device_id}_max_heating_power" - self._update_attr(mill_device) + super().__init__(coordinator, mill_device) @callback def _update_attr(self, device: MillDevice) -> None: diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 018b9466deb..57eead9be18 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -192,9 +192,9 @@ class MillSensor(MillBaseEntity, SensorEntity): mill_device: mill.Socket | mill.Heater, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, mill_device) self.entity_description = entity_description self._attr_unique_id = f"{mill_device.device_id}_{entity_description.key}" + super().__init__(coordinator, mill_device) @callback def _update_attr(self, device): 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/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8b16e9fa53d..6656afe2c8a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -236,7 +236,7 @@ CONFIG_SCHEMA = vol.Schema( MQTT_PUBLISH_SCHEMA = vol.Schema( { vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Required(ATTR_PAYLOAD): cv.string, + vol.Required(ATTR_PAYLOAD, default=None): vol.Any(cv.string, None), vol.Optional(ATTR_EVALUATE_PAYLOAD): cv.boolean, vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index c5e4f372bd6..f6fac1d2c1e 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -8,7 +8,6 @@ publish: selector: text: payload: - required: true example: "The temperature is {{ states('sensor.temperature') }}" selector: template: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index bf0bd594ea4..fc316306d56 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -238,11 +238,7 @@ }, "payload": { "name": "Payload", - "description": "The payload to publish." - }, - "payload_template": { - "name": "Payload template", - "description": "Template to render as a payload value. If a payload is provided, the template is ignored." + "description": "The payload to publish. Publishes an empty message if not provided." }, "qos": { "name": "QoS", 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/nest/manifest.json b/homeassistant/components/nest/manifest.json index cd961276082..a0d8bc06640 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], - "requirements": ["google-nest-sdm==7.1.1"] + "requirements": ["google-nest-sdm==7.1.3"] } diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index b50410cd7de..83fca0ca2d6 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "iot_class": "local_push", "loggers": ["nikohomecontrol"], - "requirements": ["nhc==0.4.4"] + "requirements": ["nhc==0.4.10"] } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 8cc81857770..02a189883bc 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_polling", "loggers": ["noaa_coops"], "quality_scale": "legacy", - "requirements": ["noaa-coops==0.1.9"] + "requirements": ["noaa-coops==0.4.0"] } diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index f6ec9dc4bf2..0af2c340960 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -169,8 +169,8 @@ class NOAATidesAndCurrentsSensor(SensorEntity): api_data = df_predictions.head() self.data = NOAATidesData( time_stamp=list(api_data.index), - hi_lo=list(api_data["hi_lo"].values), - predicted_wl=list(api_data["predicted_wl"].values), + hi_lo=list(api_data["type"].values), + predicted_wl=list(api_data["v"].values), ) _LOGGER.debug("Data = %s", api_data) _LOGGER.debug( diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 463fcc919c7..bdde3a4567e 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -494,7 +494,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { SIGNAL_STRENGTH_DECIBELS_MILLIWATT, }, NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), - NumberDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux}, NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature), NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { 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/ohme/manifest.json b/homeassistant/components/ohme/manifest.json index 602c53ced7b..100967f819f 100644 --- a/homeassistant/components/ohme/manifest.json +++ b/homeassistant/components/ohme/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["ohme==1.2.8"] + "requirements": ["ohme==1.2.9"] } diff --git a/homeassistant/components/ondilo_ico/coordinator.py b/homeassistant/components/ondilo_ico/coordinator.py index bc092ad0b9a..ff1502a89fd 100644 --- a/homeassistant/components/ondilo_ico/coordinator.py +++ b/homeassistant/components/ondilo_ico/coordinator.py @@ -34,7 +34,7 @@ class OndiloIcoCoordinator(DataUpdateCoordinator[dict[str, OndiloIcoData]]): hass, logger=_LOGGER, name=DOMAIN, - update_interval=timedelta(minutes=20), + update_interval=timedelta(hours=1), ) self.api = api 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/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 97a82fc8a1a..acb57e594b8 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -92,7 +92,7 @@ SUPPORT_ONKYO = ( DEFAULT_PLAYABLE_SOURCES = ( InputSource.from_meaning("FM"), InputSource.from_meaning("AM"), - InputSource.from_meaning("TUNER"), + InputSource.from_meaning("DAB"), ) ATTR_PRESET = "preset" 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/overseerr/const.py b/homeassistant/components/overseerr/const.py index 5c33ca3fcec..2aa0879ffed 100644 --- a/homeassistant/components/overseerr/const.py +++ b/homeassistant/components/overseerr/const.py @@ -27,7 +27,7 @@ REGISTERED_NOTIFICATIONS = ( JSON_PAYLOAD = ( '"{\\"notification_type\\":\\"{{notification_type}}\\",\\"subject\\":\\"{{subject}' '}\\",\\"message\\":\\"{{message}}\\",\\"image\\":\\"{{image}}\\",\\"{{media}}\\":' - '{\\"media_type\\":\\"{{media_type}}\\",\\"tmdb_idd\\":\\"{{media_tmdbid}}\\",\\"t' + '{\\"media_type\\":\\"{{media_type}}\\",\\"tmdb_id\\":\\"{{media_tmdbid}}\\",\\"t' 'vdb_id\\":\\"{{media_tvdbid}}\\",\\"status\\":\\"{{media_status}}\\",\\"status4k' '\\":\\"{{media_status4k}}\\"},\\"{{request}}\\":{\\"request_id\\":\\"{{request_id' '}}\\",\\"requested_by_email\\":\\"{{requestedBy_email}}\\",\\"requested_by_userna' diff --git a/homeassistant/components/overseerr/manifest.json b/homeassistant/components/overseerr/manifest.json index 396b9d7000b..6258481adcf 100644 --- a/homeassistant/components/overseerr/manifest.json +++ b/homeassistant/components/overseerr/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["python-overseerr==0.6.0"] + "requirements": ["python-overseerr==0.7.0"] } diff --git a/homeassistant/components/peblar/coordinator.py b/homeassistant/components/peblar/coordinator.py index 058f2aefb3b..36708b207c5 100644 --- a/homeassistant/components/peblar/coordinator.py +++ b/homeassistant/components/peblar/coordinator.py @@ -34,6 +34,7 @@ class PeblarRuntimeData: """Class to hold runtime data.""" data_coordinator: PeblarDataUpdateCoordinator + last_known_charging_limit = 6 system_information: PeblarSystemInformation user_configuration_coordinator: PeblarUserConfigurationDataUpdateCoordinator version_coordinator: PeblarVersionDataUpdateCoordinator @@ -137,6 +138,8 @@ class PeblarVersionDataUpdateCoordinator( class PeblarDataUpdateCoordinator(DataUpdateCoordinator[PeblarData]): """Class to manage fetching Peblar active data.""" + config_entry: PeblarConfigEntry + def __init__( self, hass: HomeAssistant, entry: PeblarConfigEntry, api: PeblarApi ) -> None: diff --git a/homeassistant/components/peblar/icons.json b/homeassistant/components/peblar/icons.json index 6244945077b..a954d112c4a 100644 --- a/homeassistant/components/peblar/icons.json +++ b/homeassistant/components/peblar/icons.json @@ -36,6 +36,9 @@ } }, "switch": { + "charge": { + "default": "mdi:ev-plug-type2" + }, "force_single_phase": { "default": "mdi:power-cycle" } diff --git a/homeassistant/components/peblar/number.py b/homeassistant/components/peblar/number.py index 1a7cec43295..0e929a63523 100644 --- a/homeassistant/components/peblar/number.py +++ b/homeassistant/components/peblar/number.py @@ -2,58 +2,27 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from typing import Any - -from peblar import PeblarApi - from homeassistant.components.number import ( NumberDeviceClass, - NumberEntity, NumberEntityDescription, + RestoreNumber, ) -from homeassistant.const import EntityCategory, UnitOfElectricCurrent -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + EntityCategory, + UnitOfElectricCurrent, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .coordinator import ( - PeblarConfigEntry, - PeblarData, - PeblarDataUpdateCoordinator, - PeblarRuntimeData, -) +from .coordinator import PeblarConfigEntry, PeblarDataUpdateCoordinator from .entity import PeblarEntity from .helpers import peblar_exception_handler PARALLEL_UPDATES = 1 -@dataclass(frozen=True, kw_only=True) -class PeblarNumberEntityDescription(NumberEntityDescription): - """Describe a Peblar number.""" - - native_max_value_fn: Callable[[PeblarRuntimeData], int] - set_value_fn: Callable[[PeblarApi, float], Awaitable[Any]] - value_fn: Callable[[PeblarData], int | None] - - -DESCRIPTIONS = [ - PeblarNumberEntityDescription( - key="charge_current_limit", - translation_key="charge_current_limit", - device_class=NumberDeviceClass.CURRENT, - entity_category=EntityCategory.CONFIG, - native_step=1, - native_min_value=6, - native_max_value_fn=lambda x: x.user_configuration_coordinator.data.user_defined_charge_limit_current, - native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, - set_value_fn=lambda x, v: x.ev_interface(charge_current_limit=int(v) * 1000), - value_fn=lambda x: round(x.ev.charge_current_limit / 1000), - ), -] - - async def async_setup_entry( hass: HomeAssistant, entry: PeblarConfigEntry, @@ -61,42 +30,101 @@ async def async_setup_entry( ) -> None: """Set up Peblar number based on a config entry.""" async_add_entities( - PeblarNumberEntity( - entry=entry, - coordinator=entry.runtime_data.data_coordinator, - description=description, - ) - for description in DESCRIPTIONS + [ + PeblarChargeCurrentLimitNumberEntity( + entry=entry, + coordinator=entry.runtime_data.data_coordinator, + ) + ] ) -class PeblarNumberEntity( +class PeblarChargeCurrentLimitNumberEntity( PeblarEntity[PeblarDataUpdateCoordinator], - NumberEntity, + RestoreNumber, ): - """Defines a Peblar number.""" + """Defines a Peblar charge current limit number. - entity_description: PeblarNumberEntityDescription + This entity is a little bit different from the other entities, any value + below 6 amps is ignored. It means the Peblar is not charging. + Peblar has assigned a dual functionality to the charge current limit + number, it is used to set the current charging value and to start/stop/pauze + the charging process. + """ + + _attr_device_class = NumberDeviceClass.CURRENT + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value = 6 + _attr_native_step = 1 + _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE + _attr_translation_key = "charge_current_limit" def __init__( self, entry: PeblarConfigEntry, coordinator: PeblarDataUpdateCoordinator, - description: PeblarNumberEntityDescription, ) -> None: - """Initialize the Peblar entity.""" - super().__init__(entry=entry, coordinator=coordinator, description=description) - self._attr_native_max_value = description.native_max_value_fn( - entry.runtime_data + """Initialize the Peblar charge current limit entity.""" + super().__init__( + entry=entry, + coordinator=coordinator, + description=NumberEntityDescription(key="charge_current_limit"), ) + configuration = entry.runtime_data.user_configuration_coordinator.data + self._attr_native_max_value = configuration.user_defined_charge_limit_current - @property - def native_value(self) -> int | None: - """Return the number value.""" - return self.entity_description.value_fn(self.coordinator.data) + async def async_added_to_hass(self) -> None: + """Load the last known state when adding this entity.""" + if ( + (last_state := await self.async_get_last_state()) + and (last_number_data := await self.async_get_last_number_data()) + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + and last_number_data.native_value + ): + self._attr_native_value = last_number_data.native_value + # Set the last known charging limit in the runtime data the + # start/stop/pauze functionality needs it in order to restore + # the last known charging limits when charging is resumed. + self.coordinator.config_entry.runtime_data.last_known_charging_limit = int( + last_number_data.native_value + ) + await super().async_added_to_hass() + self._handle_coordinator_update() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle coordinator update. + + Ignore any update that provides a ampere value that is below the + minimum value (6 amps). It means the Peblar is currently not charging. + """ + if ( + current_charge_limit := round( + self.coordinator.data.ev.charge_current_limit / 1000 + ) + ) < 6: + return + self._attr_native_value = current_charge_limit + # Update the last known charging limit in the runtime data the + # start/stop/pauze functionality needs it in order to restore + # the last known charging limits when charging is resumed. + self.coordinator.config_entry.runtime_data.last_known_charging_limit = ( + current_charge_limit + ) + super()._handle_coordinator_update() @peblar_exception_handler async def async_set_native_value(self, value: float) -> None: - """Change to new number value.""" - await self.entity_description.set_value_fn(self.coordinator.api, value) + """Change the current charging value.""" + # If charging is currently disabled (below 6 amps), just set the value + # as the native value and the last known charging limit in the runtime + # data. So we can pick it up once charging gets enabled again. + if self.coordinator.data.ev.charge_current_limit < 6000: + self._attr_native_value = int(value) + self.coordinator.config_entry.runtime_data.last_known_charging_limit = int( + value + ) + self.async_write_ha_state() + return + await self.coordinator.api.ev_interface(charge_current_limit=int(value) * 1000) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/peblar/strings.json b/homeassistant/components/peblar/strings.json index a33667fa533..4a1500e54c5 100644 --- a/homeassistant/components/peblar/strings.json +++ b/homeassistant/components/peblar/strings.json @@ -153,6 +153,9 @@ } }, "switch": { + "charge": { + "name": "Charge" + }, "force_single_phase": { "name": "Force single phase" } diff --git a/homeassistant/components/peblar/switch.py b/homeassistant/components/peblar/switch.py index e56c2fcdaec..74a42ddc47d 100644 --- a/homeassistant/components/peblar/switch.py +++ b/homeassistant/components/peblar/switch.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from peblar import PeblarApi +from peblar import PeblarEVInterface from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory @@ -31,7 +31,19 @@ class PeblarSwitchEntityDescription(SwitchEntityDescription): has_fn: Callable[[PeblarRuntimeData], bool] = lambda x: True is_on_fn: Callable[[PeblarData], bool] - set_fn: Callable[[PeblarApi, bool], Awaitable[Any]] + set_fn: Callable[[PeblarDataUpdateCoordinator, bool], Awaitable[Any]] + + +def _async_peblar_charge( + coordinator: PeblarDataUpdateCoordinator, on: bool +) -> Awaitable[PeblarEVInterface]: + """Set the charge state.""" + charge_current_limit = 0 + if on: + charge_current_limit = ( + coordinator.config_entry.runtime_data.last_known_charging_limit * 1000 + ) + return coordinator.api.ev_interface(charge_current_limit=charge_current_limit) DESCRIPTIONS = [ @@ -44,7 +56,14 @@ DESCRIPTIONS = [ and x.user_configuration_coordinator.data.connected_phases > 1 ), is_on_fn=lambda x: x.ev.force_single_phase, - set_fn=lambda x, on: x.ev_interface(force_single_phase=on), + set_fn=lambda x, on: x.api.ev_interface(force_single_phase=on), + ), + PeblarSwitchEntityDescription( + key="charge", + translation_key="charge", + entity_category=EntityCategory.CONFIG, + is_on_fn=lambda x: (x.ev.charge_current_limit >= 6000), + set_fn=_async_peblar_charge, ), ] @@ -82,11 +101,11 @@ class PeblarSwitchEntity( @peblar_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.set_fn(self.coordinator.api, True) + await self.entity_description.set_fn(self.coordinator, True) await self.coordinator.async_request_refresh() @peblar_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self.entity_description.set_fn(self.coordinator.api, False) + await self.entity_description.set_fn(self.coordinator, False) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index a100103b029..f1cc7c6c11d 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -82,7 +82,7 @@ def migrate_sensor_entities( # Migrating opentherm_outdoor_temperature # to opentherm_outdoor_air_temperature sensor - for device_id, device in coordinator.data.devices.items(): + for device_id, device in coordinator.data.items(): if device["dev_class"] != "heater_central": continue diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 539fa243d6c..a4c6e051c78 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -100,11 +100,7 @@ async def async_setup_entry( async_add_entities( PlugwiseBinarySensorEntity(coordinator, device_id, description) for device_id in coordinator.new_devices - if ( - binary_sensors := coordinator.data.devices[device_id].get( - "binary_sensors" - ) - ) + if (binary_sensors := coordinator.data[device_id].get("binary_sensors")) for description in BINARY_SENSORS if description.key in binary_sensors ) @@ -141,7 +137,8 @@ class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): return None attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES} - if notify := self.coordinator.data.gateway["notifications"]: + gateway_id = self.coordinator.api.gateway_id + if notify := self.coordinator.data[gateway_id]["notifications"]: for details in notify.values(): for msg_type, msg in details.items(): msg_type = msg_type.lower() diff --git a/homeassistant/components/plugwise/button.py b/homeassistant/components/plugwise/button.py index 8a05ede3496..139b358162c 100644 --- a/homeassistant/components/plugwise/button.py +++ b/homeassistant/components/plugwise/button.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PlugwiseConfigEntry -from .const import GATEWAY_ID, REBOOT +from .const import REBOOT from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity from .util import plugwise_command @@ -24,11 +24,10 @@ async def async_setup_entry( """Set up the Plugwise buttons from a ConfigEntry.""" coordinator = entry.runtime_data - gateway = coordinator.data.gateway async_add_entities( PlugwiseButtonEntity(coordinator, device_id) - for device_id in coordinator.data.devices - if device_id == gateway[GATEWAY_ID] and REBOOT in gateway + for device_id in coordinator.data + if device_id == coordinator.api.gateway_id and coordinator.api.reboot ) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 3caed1e7bc2..7abdfcfde54 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -41,18 +41,17 @@ async def async_setup_entry( if not coordinator.new_devices: return - if coordinator.data.gateway["smile_name"] == "Adam": + if coordinator.api.smile_name == "Adam": async_add_entities( PlugwiseClimateEntity(coordinator, device_id) for device_id in coordinator.new_devices - if coordinator.data.devices[device_id]["dev_class"] == "climate" + if coordinator.data[device_id]["dev_class"] == "climate" ) else: async_add_entities( PlugwiseClimateEntity(coordinator, device_id) for device_id in coordinator.new_devices - if coordinator.data.devices[device_id]["dev_class"] - in MASTER_THERMOSTATS + if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS ) _add_entities() @@ -77,10 +76,8 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): super().__init__(coordinator, device_id) self._attr_unique_id = f"{device_id}-climate" - self._devices = coordinator.data.devices - self._gateway = coordinator.data.gateway - gateway_id: str = self._gateway["gateway_id"] - self._gateway_data = self._devices[gateway_id] + gateway_id: str = coordinator.api.gateway_id + self._gateway_data = coordinator.data[gateway_id] self._location = device_id if (location := self.device.get("location")) is not None: @@ -88,7 +85,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): # Determine supported features self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if self._gateway["cooling_present"] and self._gateway["smile_name"] != "Adam": + if ( + self.coordinator.api.cooling_present + and coordinator.api.smile_name != "Adam" + ): self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) @@ -170,7 +170,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): if "available_schedules" in self.device: hvac_modes.append(HVACMode.AUTO) - if self._gateway["cooling_present"]: + if self.coordinator.api.cooling_present: if "regulation_modes" in self._gateway_data: if self._gateway_data["select_regulation_mode"] == "cooling": hvac_modes.append(HVACMode.COOL) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index a94000934eb..bf33d4c4a0f 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -59,8 +59,6 @@ def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema: schema = schema.extend( { vol.Required(CONF_HOST): str, - # Port under investigation for removal (hence not added in #132878) - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, vol.Required(CONF_USERNAME, default=SMILE): vol.In( {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH} ), @@ -197,6 +195,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: + user_input[CONF_PORT] = DEFAULT_PORT if self.discovery_info: user_input[CONF_HOST] = self.discovery_info.host user_input[CONF_PORT] = self.discovery_info.port diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 5e4dea5586b..176ae39b1ba 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -17,7 +17,6 @@ FLOW_SMILE: Final = "smile (Adam/Anna/P1)" FLOW_STRETCH: Final = "stretch (Stretch)" FLOW_TYPE: Final = "flow_type" GATEWAY: Final = "gateway" -GATEWAY_ID: Final = "gateway_id" LOCATION: Final = "location" PW_TYPE: Final = "plugwise_type" REBOOT: Final = "reboot" diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 7ac0cc21c51..9a85ae2a5df 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -3,7 +3,7 @@ from datetime import timedelta from packaging.version import Version -from plugwise import PlugwiseData, Smile +from plugwise import GwEntityData, Smile from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, @@ -22,10 +22,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER +from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER -class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): +class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]): """Class to manage fetching Plugwise data from single endpoint.""" _connected: bool = False @@ -63,10 +63,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): """Connect to the Plugwise Smile.""" version = await self.api.connect() self._connected = isinstance(version, Version) - if self._connected: - self.api.get_all_gateway_entities() - async def _async_update_data(self) -> PlugwiseData: + async def _async_update_data(self) -> dict[str, GwEntityData]: """Fetch data from Plugwise.""" try: if not self._connected: @@ -101,26 +99,28 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): self._async_add_remove_devices(data, self.config_entry) return data - def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + def _async_add_remove_devices( + self, data: dict[str, GwEntityData], entry: ConfigEntry + ) -> None: """Add new Plugwise devices, remove non-existing devices.""" # Check for new or removed devices - self.new_devices = set(data.devices) - self._current_devices - removed_devices = self._current_devices - set(data.devices) - self._current_devices = set(data.devices) + self.new_devices = set(data) - self._current_devices + removed_devices = self._current_devices - set(data) + self._current_devices = set(data) if removed_devices: self._async_remove_devices(data, entry) - def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + def _async_remove_devices( + self, data: dict[str, GwEntityData], entry: ConfigEntry + ) -> None: """Clean registries when removed devices found.""" device_reg = dr.async_get(self.hass) device_list = dr.async_entries_for_config_entry( device_reg, self.config_entry.entry_id ) # First find the Plugwise via_device - gateway_device = device_reg.async_get_device( - {(DOMAIN, data.gateway[GATEWAY_ID])} - ) + gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)}) assert gateway_device is not None via_device_id = gateway_device.id @@ -130,7 +130,7 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): if identifier[0] == DOMAIN: if ( device_entry.via_device_id == via_device_id - and identifier[1] not in data.devices + and identifier[1] not in data ): device_reg.async_update_device( device_entry.id, remove_config_entry_id=entry.entry_id diff --git a/homeassistant/components/plugwise/diagnostics.py b/homeassistant/components/plugwise/diagnostics.py index 47ff7d1a9fb..a576e60dbe1 100644 --- a/homeassistant/components/plugwise/diagnostics.py +++ b/homeassistant/components/plugwise/diagnostics.py @@ -14,7 +14,4 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator = entry.runtime_data - return { - "devices": coordinator.data.devices, - "gateway": coordinator.data.gateway, - } + return coordinator.data diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 3f63abaff43..39838c38fde 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -34,7 +34,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): if entry := self.coordinator.config_entry: configuration_url = f"http://{entry.data[CONF_HOST]}" - data = coordinator.data.devices[device_id] + data = coordinator.data[device_id] connections = set() if mac := data.get("mac_address"): connections.add((CONNECTION_NETWORK_MAC, mac)) @@ -48,18 +48,18 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): manufacturer=data.get("vendor"), model=data.get("model"), model_id=data.get("model_id"), - name=coordinator.data.gateway["smile_name"], + name=coordinator.api.smile_name, sw_version=data.get("firmware"), hw_version=data.get("hardware"), ) - if device_id != coordinator.data.gateway["gateway_id"]: + if device_id != coordinator.api.gateway_id: self._attr_device_info.update( { ATTR_NAME: data.get("name"), ATTR_VIA_DEVICE: ( DOMAIN, - str(self.coordinator.data.gateway["gateway_id"]), + str(self.coordinator.api.gateway_id), ), } ) @@ -68,7 +68,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): def available(self) -> bool: """Return if entity is available.""" return ( - self._dev_id in self.coordinator.data.devices + self._dev_id in self.coordinator.data and ("available" not in self.device or self.device["available"] is True) and super().available ) @@ -76,4 +76,4 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): @property def device(self) -> GwEntityData: """Return data for this device.""" - return self.coordinator.data.devices[self._dev_id] + return self.coordinator.data[self._dev_id] diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f7bd646f801..983ff10b0a6 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["plugwise"], "quality_scale": "platinum", - "requirements": ["plugwise==1.6.4"], + "requirements": ["plugwise==1.7.1"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 1d0b1382c24..2de49f17d4a 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -73,7 +73,7 @@ async def async_setup_entry( PlugwiseNumberEntity(coordinator, device_id, description) for device_id in coordinator.new_devices for description in NUMBER_TYPES - if description.key in coordinator.data.devices[device_id] + if description.key in coordinator.data[device_id] ) _add_entities() diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index ff268d8eded..307091f0ff9 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -71,7 +71,7 @@ async def async_setup_entry( PlugwiseSelectEntity(coordinator, device_id, description) for device_id in coordinator.new_devices for description in SELECT_TYPES - if description.options_key in coordinator.data.devices[device_id] + if description.options_key in coordinator.data[device_id] ) _add_entities() diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 14b42682376..8b630c39878 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -420,7 +420,7 @@ async def async_setup_entry( async_add_entities( PlugwiseSensorEntity(coordinator, device_id, description) for device_id in coordinator.new_devices - if (sensors := coordinator.data.devices[device_id].get("sensors")) + if (sensors := coordinator.data[device_id].get("sensors")) for description in SENSORS if description.key in sensors ) diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index ea6d6f18b7f..86496a4311e 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -72,7 +72,7 @@ async def async_setup_entry( async_add_entities( PlugwiseSwitchEntity(coordinator, device_id, description) for device_id in coordinator.new_devices - if (switches := coordinator.data.devices[device_id].get("switches")) + if (switches := coordinator.data[device_id].get("switches")) for description in SWITCHES if description.key in switches ) diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index aed2fcf8508..8958913bce6 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -766,7 +766,7 @@ def _sorted_states_to_dict( attr_cache, start_time_ts, entity_id, - prev_state, # type: ignore[arg-type] + prev_state, first_state[last_updated_ts_idx], no_attributes, ) 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/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index fb3c096ee41..505358a07f7 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.11.9"] + "requirements": ["reolink-aio==0.11.10"] } diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 71e80086833..9c9104258a8 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -17,7 +17,7 @@ PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.SENSOR, Platform.SWITCH async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" - coordinator = RidwellDataUpdateCoordinator(hass, name=entry.title) + coordinator = RidwellDataUpdateCoordinator(hass, entry) await coordinator.async_initialize() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/ridwell/coordinator.py b/homeassistant/components/ridwell/coordinator.py index 28190522c76..336a71bc67f 100644 --- a/homeassistant/components/ridwell/coordinator.py +++ b/homeassistant/components/ridwell/coordinator.py @@ -29,7 +29,7 @@ class RidwellDataUpdateCoordinator( config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, *, name: str) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize.""" # These will be filled in by async_initialize; we give them these defaults to # avoid arduous typing checks down the line: @@ -37,7 +37,13 @@ class RidwellDataUpdateCoordinator( self.dashboard_url = "" self.user_id = "" - super().__init__(hass, LOGGER, name=name, update_interval=UPDATE_INTERVAL) + super().__init__( + hass, + LOGGER, + config_entry=config_entry, + name=config_entry.title, + update_interval=UPDATE_INTERVAL, + ) async def _async_update_data(self) -> dict[str, list[RidwellPickupEvent]]: """Fetch the latest data from the source.""" diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 7005344614c..8968ac020a2 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -23,8 +23,8 @@ "invalid_email": "There is no account associated with the email you entered, please try again.", "invalid_email_format": "There is an issue with the formatting of your email - please try again.", "too_frequent_code_requests": "You have attempted to request too many codes. Try again later.", - "unknown_roborock": "There was an unknown roborock exception - please check your logs.", - "unknown_url": "There was an issue determining the correct url for your roborock account - please check your logs.", + "unknown_roborock": "There was an unknown Roborock exception - please check your logs.", + "unknown_url": "There was an issue determining the correct URL for your Roborock account - please check your logs.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { @@ -436,11 +436,11 @@ "services": { "get_maps": { "name": "Get maps", - "description": "Get the map and room information of your device." + "description": "Retrieves the map and room information of your device." }, "set_vacuum_goto_position": { "name": "Go to position", - "description": "Send the vacuum to a specific position.", + "description": "Sends the vacuum to a specific position.", "fields": { "x": { "name": "X-coordinate", @@ -454,7 +454,7 @@ }, "get_vacuum_current_position": { "name": "Get current position", - "description": "Get the current position of the vacuum." + "description": "Retrieves the current position of the vacuum." } } } diff --git a/homeassistant/components/romy/__init__.py b/homeassistant/components/romy/__init__.py index 352f5f3715a..be227645122 100644 --- a/homeassistant/components/romy/__init__.py +++ b/homeassistant/components/romy/__init__.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_HOST], config_entry.data.get(CONF_PASSWORD, "") ) - coordinator = RomyVacuumCoordinator(hass, new_romy) + coordinator = RomyVacuumCoordinator(hass, config_entry, new_romy) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator diff --git a/homeassistant/components/romy/coordinator.py b/homeassistant/components/romy/coordinator.py index 5868eae70e2..d666ec44f80 100644 --- a/homeassistant/components/romy/coordinator.py +++ b/homeassistant/components/romy/coordinator.py @@ -2,6 +2,7 @@ from romy import RomyRobot +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -11,9 +12,19 @@ from .const import DOMAIN, LOGGER, UPDATE_INTERVAL class RomyVacuumCoordinator(DataUpdateCoordinator[None]): """ROMY Vacuum Coordinator.""" - def __init__(self, hass: HomeAssistant, romy: RomyRobot) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, romy: RomyRobot + ) -> None: """Initialize.""" - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + super().__init__( + hass, + LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) self.hass = hass self.romy = romy diff --git a/homeassistant/components/rova/__init__.py b/homeassistant/components/rova/__init__.py index 64f0e787a4b..ecde0578772 100644 --- a/homeassistant/components/rova/__init__.py +++ b/homeassistant/components/rova/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) raise ConfigEntryError("Rova does not collect garbage in this area") - coordinator = RovaCoordinator(hass, api) + coordinator = RovaCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/rova/coordinator.py b/homeassistant/components/rova/coordinator.py index ecd91cad823..a48048d32c3 100644 --- a/homeassistant/components/rova/coordinator.py +++ b/homeassistant/components/rova/coordinator.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from rova.rova import Rova +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import get_time_zone @@ -16,11 +17,16 @@ EUROPE_AMSTERDAM_ZONE_INFO = get_time_zone("Europe/Amsterdam") class RovaCoordinator(DataUpdateCoordinator[dict[str, datetime]]): """Class to manage fetching Rova data.""" - def __init__(self, hass: HomeAssistant, api: Rova) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: Rova + ) -> None: """Initialize.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(hours=12), ) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 6d4e491b839..e416cd35765 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -44,14 +44,11 @@ from .const import ( UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, ) -from .coordinator import SamsungTVDataUpdateCoordinator +from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] -SamsungTVConfigEntry = ConfigEntry[SamsungTVDataUpdateCoordinator] - - @callback def _async_get_device_bridge( hass: HomeAssistant, data: dict[str, Any] @@ -165,7 +162,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) -> entry.async_on_unload(debounced_reloader.async_shutdown) entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) - coordinator = SamsungTVDataUpdateCoordinator(hass, bridge) + coordinator = SamsungTVDataUpdateCoordinator(hass, entry, bridge) 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/samsungtv/coordinator.py b/homeassistant/components/samsungtv/coordinator.py index 92d8dc8fa84..443e62b13fb 100644 --- a/homeassistant/components/samsungtv/coordinator.py +++ b/homeassistant/components/samsungtv/coordinator.py @@ -15,17 +15,25 @@ from .const import DOMAIN, LOGGER SCAN_INTERVAL = 10 +type SamsungTVConfigEntry = ConfigEntry[SamsungTVDataUpdateCoordinator] + class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator for the SamsungTV integration.""" - config_entry: ConfigEntry + config_entry: SamsungTVConfigEntry - def __init__(self, hass: HomeAssistant, bridge: SamsungTVBridge) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: SamsungTVConfigEntry, + bridge: SamsungTVBridge, + ) -> None: """Initialize the coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=SCAN_INTERVAL), ) diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index ebca8d2543b..667d23ba631 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -8,8 +8,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from . import SamsungTVConfigEntry from .const import CONF_SESSION_ID +from .coordinator import SamsungTVConfigEntry TO_REDACT = {CONF_TOKEN, CONF_SESSION_ID} diff --git a/homeassistant/components/samsungtv/helpers.py b/homeassistant/components/samsungtv/helpers.py index 4e8dd00d486..b4075b8117f 100644 --- a/homeassistant/components/samsungtv/helpers.py +++ b/homeassistant/components/samsungtv/helpers.py @@ -7,9 +7,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import SamsungTVConfigEntry from .bridge import SamsungTVBridge from .const import DOMAIN +from .coordinator import SamsungTVConfigEntry @callback diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 7180e8a0c1a..9db9916c24a 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -34,10 +34,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.async_ import create_eager_task -from . import SamsungTVConfigEntry from .bridge import SamsungTVWSBridge from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, LOGGER -from .coordinator import SamsungTVDataUpdateCoordinator +from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator from .entity import SamsungTVEntity SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} diff --git a/homeassistant/components/samsungtv/remote.py b/homeassistant/components/samsungtv/remote.py index 401a5d383f0..3d2529153be 100644 --- a/homeassistant/components/samsungtv/remote.py +++ b/homeassistant/components/samsungtv/remote.py @@ -9,8 +9,8 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SamsungTVConfigEntry from .const import LOGGER +from .coordinator import SamsungTVConfigEntry from .entity import SamsungTVEntity diff --git a/homeassistant/components/sanix/__init__.py b/homeassistant/components/sanix/__init__.py index c8c5567eedc..60cc5b56f2e 100644 --- a/homeassistant/components/sanix/__init__.py +++ b/homeassistant/components/sanix/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token = entry.data[CONF_TOKEN] sanix_api = Sanix(serial_no, token) - coordinator = SanixCoordinator(hass, sanix_api) + coordinator = SanixCoordinator(hass, entry, sanix_api) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/sanix/coordinator.py b/homeassistant/components/sanix/coordinator.py index d6362337a38..64d28fa9191 100644 --- a/homeassistant/components/sanix/coordinator.py +++ b/homeassistant/components/sanix/coordinator.py @@ -21,10 +21,16 @@ class SanixCoordinator(DataUpdateCoordinator[Measurement]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, sanix_api: Sanix) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, sanix_api: Sanix + ) -> None: """Initialize coordinator.""" super().__init__( - hass, _LOGGER, name=MANUFACTURER, update_interval=timedelta(hours=1) + hass, + _LOGGER, + config_entry=config_entry, + name=MANUFACTURER, + update_interval=timedelta(hours=1), ) self._sanix_api = sanix_api diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 59a87c419e0..c46aca548c8 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -590,7 +590,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SIGNAL_STRENGTH_DECIBELS_MILLIWATT, }, SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), - SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux}, SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature), SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 695ca179966..235a5338cb6 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SeventeenTrackError as err: raise ConfigEntryNotReady from err - seventeen_coordinator = SeventeenTrackCoordinator(hass, client) + seventeen_coordinator = SeventeenTrackCoordinator(hass, entry, client) await seventeen_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/seventeentrack/coordinator.py b/homeassistant/components/seventeentrack/coordinator.py index 3e27f9f0369..107f1d48a21 100644 --- a/homeassistant/components/seventeentrack/coordinator.py +++ b/homeassistant/components/seventeentrack/coordinator.py @@ -34,11 +34,17 @@ class SeventeenTrackCoordinator(DataUpdateCoordinator[SeventeenTrackData]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, client: SeventeenTrackClient) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + client: SeventeenTrackClient, + ) -> None: """Initialize.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL, ) diff --git a/homeassistant/components/shelly/bluetooth/__init__.py b/homeassistant/components/shelly/bluetooth/__init__.py index 366d5c51d25..d7eb020d671 100644 --- a/homeassistant/components/shelly/bluetooth/__init__.py +++ b/homeassistant/components/shelly/bluetooth/__init__.py @@ -25,7 +25,7 @@ async def async_connect_scanner( ) -> CALLBACK_TYPE: """Connect scanner.""" device = coordinator.device - entry = coordinator.entry + entry = coordinator.config_entry source = format_mac(coordinator.mac).upper() scanner = create_scanner(source, entry.title) unload_callbacks = [ 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/coordinator.py b/homeassistant/components/shelly/coordinator.py index f2a01240f70..ad35ec32299 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -95,6 +95,8 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( ): """Coordinator for a Shelly device.""" + config_entry: ShellyConfigEntry + def __init__( self, hass: HomeAssistant, @@ -103,7 +105,6 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( update_interval: float, ) -> None: """Initialize the Shelly device coordinator.""" - self.entry = entry self.device = device self.device_id: str | None = None self._pending_platforms: list[Platform] | None = None @@ -112,7 +113,13 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( # The device has come online at least once. In the case of a sleeping RPC # device, this means that the device has connected to the WS server at least once. self._came_online_once = False - super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) + super().__init__( + hass, + LOGGER, + config_entry=entry, + name=device_name, + update_interval=interval_td, + ) self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, @@ -130,12 +137,12 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( @cached_property def model(self) -> str: """Model of the device.""" - return cast(str, self.entry.data["model"]) + return cast(str, self.config_entry.data["model"]) @cached_property def mac(self) -> str: """Mac address of the device.""" - return cast(str, self.entry.unique_id) + return cast(str, self.config_entry.unique_id) @property def sw_version(self) -> str: @@ -145,14 +152,14 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( @property def sleep_period(self) -> int: """Sleep period of the device.""" - return self.entry.data.get(CONF_SLEEP_PERIOD, 0) + return self.config_entry.data.get(CONF_SLEEP_PERIOD, 0) def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" self._pending_platforms = pending_platforms dev_reg = dr.async_get(self.hass) device_entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, + config_entry_id=self.config_entry.entry_id, name=self.name, connections={(CONNECTION_NETWORK_MAC, self.mac)}, identifiers={(DOMAIN, self.mac)}, @@ -160,8 +167,8 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( model=MODEL_NAMES.get(self.model), model_id=self.model, sw_version=self.sw_version, - hw_version=f"gen{get_device_entry_gen(self.entry)}", - configuration_url=f"http://{get_host(self.entry.data[CONF_HOST])}:{get_http_port(self.entry.data)}", + hw_version=f"gen{get_device_entry_gen(self.config_entry)}", + configuration_url=f"http://{get_host(self.config_entry.data[CONF_HOST])}:{get_http_port(self.config_entry.data)}", ) self.device_id = device_entry.id @@ -179,18 +186,18 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( LOGGER.debug("Connecting to Shelly Device - %s", self.name) try: await self.device.initialize() - update_device_fw_info(self.hass, self.device, self.entry) + update_device_fw_info(self.hass, self.device, self.config_entry) except (DeviceConnectionError, MacAddressMismatchError) as err: LOGGER.debug( "Error connecting to Shelly device %s, error: %r", self.name, err ) return False except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + self.config_entry.async_start_reauth(self.hass) return False if not self.device.firmware_supported: - async_create_issue_unsupported_firmware(self.hass, self.entry) + async_create_issue_unsupported_firmware(self.hass, self.config_entry) return False if not self._pending_platforms: @@ -200,7 +207,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( platforms = self._pending_platforms self._pending_platforms = None - data = {**self.entry.data} + data = {**self.config_entry.data} # Update sleep_period old_sleep_period = data[CONF_SLEEP_PERIOD] @@ -211,10 +218,12 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( if new_sleep_period != old_sleep_period: data[CONF_SLEEP_PERIOD] = new_sleep_period - self.hass.config_entries.async_update_entry(self.entry, data=data) + self.hass.config_entries.async_update_entry(self.config_entry, data=data) # Resume platform setup - await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, platforms + ) return True @@ -222,7 +231,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( """Reload entry.""" self._debounced_reload.async_cancel() LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) + await self.hass.config_entries.async_reload(self.config_entry.entry_id) async def async_shutdown_device_and_start_reauth(self) -> None: """Shutdown Shelly device and start reauth flow.""" @@ -230,7 +239,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( # and won't be able to send commands to the device self.last_update_success = False await self.shutdown() - self.entry.async_start_reauth(self.hass) + self.config_entry.async_start_reauth(self.hass) class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): @@ -240,9 +249,8 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self, hass: HomeAssistant, entry: ShellyConfigEntry, device: BlockDevice ) -> None: """Initialize the Shelly block device coordinator.""" - self.entry = entry - if self.sleep_period: - update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period + if sleep_period := entry.data.get(CONF_SLEEP_PERIOD, 0): + update_interval = UPDATE_PERIOD_MULTIPLIER * sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] @@ -385,7 +393,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): LOGGER.debug("Shelly %s handle update, type: %s", self.name, update_type) if update_type is BlockUpdateType.ONLINE: self._came_online_once = True - self.entry.async_create_background_task( + self.config_entry.async_create_background_task( self.hass, self._async_device_connect_task(), "block device online", @@ -415,7 +423,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): learn_more_url="https://www.home-assistant.io/integrations/shelly/#shelly-device-configuration-generation-1", translation_key="push_update_failure", translation_placeholders={ - "device_name": self.entry.title, + "device_name": self.config_entry.title, "ip_address": self.device.ip_address, }, ) @@ -462,7 +470,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): except InvalidAuthError: await self.async_shutdown_device_and_start_reauth() else: - update_device_fw_info(self.hass, self.device, self.entry) + update_device_fw_info(self.hass, self.device, self.config_entry) class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): @@ -472,9 +480,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self, hass: HomeAssistant, entry: ShellyConfigEntry, device: RpcDevice ) -> None: """Initialize the Shelly RPC device coordinator.""" - self.entry = entry - if self.sleep_period: - update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period + if sleep_period := entry.data.get(CONF_SLEEP_PERIOD, 0): + update_interval = UPDATE_PERIOD_MULTIPLIER * sleep_period else: update_interval = RPC_RECONNECT_INTERVAL super().__init__(hass, entry, device, update_interval) @@ -514,9 +521,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): ): return False - data = {**self.entry.data} + data = {**self.config_entry.data} data[CONF_SLEEP_PERIOD] = wakeup_period - self.hass.config_entries.async_update_entry(self.entry, data=data) + self.hass.config_entries.async_update_entry(self.config_entry, data=data) update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period self.update_interval = timedelta(seconds=update_interval) @@ -693,7 +700,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): async def _async_connect_ble_scanner(self) -> None: """Connect BLE scanner.""" - ble_scanner_mode = self.entry.options.get( + ble_scanner_mode = self.config_entry.options.get( CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED ) if ble_scanner_mode == BLEScannerMode.DISABLED and self.connected: @@ -719,7 +726,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): ): LOGGER.debug("Device %s already connected/connecting", self.name) return - self._connect_task = self.entry.async_create_background_task( + self._connect_task = self.config_entry.async_create_background_task( self.hass, self._async_device_connect_task(), "rpc device online", @@ -736,13 +743,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self._came_online_once = True self._async_handle_rpc_device_online() elif update_type is RpcUpdateType.INITIALIZED: - self.entry.async_create_background_task( + self.config_entry.async_create_background_task( self.hass, self._async_connected(), "rpc device init", eager_start=True ) # Make sure entities are marked available self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: - self.entry.async_create_background_task( + self.config_entry.async_create_background_task( self.hass, self._async_disconnected(True), "rpc device disconnected", @@ -753,7 +760,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: - update_device_fw_info(self.hass, self.device, self.entry) + update_device_fw_info(self.hass, self.device, self.config_entry) elif update_type is RpcUpdateType.EVENT and (event := self.device.event): self._async_device_event_handler(event) @@ -763,7 +770,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected - self.entry.async_create_task( + self.config_entry.async_create_task( self.hass, self._async_connected(), eager_start=True ) @@ -775,7 +782,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): await async_stop_scanner(self.device) await super().shutdown() except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + self.config_entry.async_start_reauth(self.hass) return except DeviceConnectionError as err: # If the device is restarting or has gone offline before diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index e0d8c03ffc4..4cfb49b680f 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aioshelly"], - "requirements": ["aioshelly==12.3.2"], + "requirements": ["aioshelly==12.4.1"], "zeroconf": [ { "type": "_http._tcp.local.", 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/simplefin/__init__.py b/homeassistant/components/simplefin/__init__.py index c47b3118415..1fe2f2a6189 100644 --- a/homeassistant/components/simplefin/__init__.py +++ b/homeassistant/components/simplefin/__init__.py @@ -4,12 +4,11 @@ from __future__ import annotations from simplefin4py import SimpleFin -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import CONF_ACCESS_URL -from .coordinator import SimpleFinDataUpdateCoordinator +from .coordinator import SimpleFinConfigEntry, SimpleFinDataUpdateCoordinator PLATFORMS: list[str] = [ Platform.BINARY_SENSOR, @@ -17,20 +16,17 @@ PLATFORMS: list[str] = [ ] -type SimpleFinConfigEntry = ConfigEntry[SimpleFinDataUpdateCoordinator] - - async def async_setup_entry(hass: HomeAssistant, entry: SimpleFinConfigEntry) -> bool: """Set up from a config entry.""" access_url = entry.data[CONF_ACCESS_URL] sf_client = SimpleFin(access_url) - sf_coordinator = SimpleFinDataUpdateCoordinator(hass, sf_client) + sf_coordinator = SimpleFinDataUpdateCoordinator(hass, entry, sf_client) await sf_coordinator.async_config_entry_first_refresh() entry.runtime_data = sf_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: SimpleFinConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/simplefin/binary_sensor.py b/homeassistant/components/simplefin/binary_sensor.py index 5805fc370b6..66d920fb309 100644 --- a/homeassistant/components/simplefin/binary_sensor.py +++ b/homeassistant/components/simplefin/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpleFinConfigEntry +from .coordinator import SimpleFinConfigEntry from .entity import SimpleFinEntity diff --git a/homeassistant/components/simplefin/coordinator.py b/homeassistant/components/simplefin/coordinator.py index 7fa5aedb7a1..08e9732c6b7 100644 --- a/homeassistant/components/simplefin/coordinator.py +++ b/homeassistant/components/simplefin/coordinator.py @@ -15,17 +15,22 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import LOGGER +type SimpleFinConfigEntry = ConfigEntry[SimpleFinDataUpdateCoordinator] + class SimpleFinDataUpdateCoordinator(DataUpdateCoordinator[FinancialData]): """Data update coordinator for the SimpleFIN integration.""" - config_entry: ConfigEntry + config_entry: SimpleFinConfigEntry - def __init__(self, hass: HomeAssistant, client: SimpleFin) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: SimpleFinConfigEntry, client: SimpleFin + ) -> None: """Initialize the coordinator.""" super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name="simplefin", update_interval=timedelta(hours=4), ) diff --git a/homeassistant/components/simplefin/sensor.py b/homeassistant/components/simplefin/sensor.py index b2167a2c014..51a96bae2be 100644 --- a/homeassistant/components/simplefin/sensor.py +++ b/homeassistant/components/simplefin/sensor.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import SimpleFinConfigEntry +from .coordinator import SimpleFinConfigEntry from .entity import SimpleFinEntity diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 0282ad40254..5baa4ad83ad 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Unable to connect to Skybell service: {ex}") from ex device_coordinators: list[SkybellDataUpdateCoordinator] = [ - SkybellDataUpdateCoordinator(hass, device) for device in devices + SkybellDataUpdateCoordinator(hass, entry, device) for device in devices ] await asyncio.gather( *[ diff --git a/homeassistant/components/skybell/coordinator.py b/homeassistant/components/skybell/coordinator.py index 55e34df5c63..48e67c63ac9 100644 --- a/homeassistant/components/skybell/coordinator.py +++ b/homeassistant/components/skybell/coordinator.py @@ -16,11 +16,14 @@ class SkybellDataUpdateCoordinator(DataUpdateCoordinator[None]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, device: SkybellDevice) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, device: SkybellDevice + ) -> None: """Initialize the coordinator.""" super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name=device.name, update_interval=timedelta(seconds=30), ) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 4f54b4cd305..565611fe169 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -94,8 +94,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_migrate_unique_ids(hass, entry, gateway) - coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) - pause_coordinator = SleepIQPauseUpdateCoordinator(hass, gateway, email) + coordinator = SleepIQDataUpdateCoordinator(hass, entry, gateway) + pause_coordinator = SleepIQPauseUpdateCoordinator(hass, entry, gateway) # Call the SleepIQ API to refresh data await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index 7fe4f964b36..46b754976e5 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -7,6 +7,8 @@ import logging from asyncsleepiq import AsyncSleepIQ +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -19,17 +21,20 @@ LONGER_UPDATE_INTERVAL = timedelta(minutes=5) class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): """SleepIQ data update coordinator.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: AsyncSleepIQ, - username: str, ) -> None: """Initialize coordinator.""" super().__init__( hass, _LOGGER, - name=f"{username}@SleepIQ", + config_entry=config_entry, + name=f"{config_entry.data[CONF_USERNAME]}@SleepIQ", update_interval=UPDATE_INTERVAL, ) self.client = client @@ -45,17 +50,20 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): """SleepIQ data update coordinator.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: AsyncSleepIQ, - username: str, ) -> None: """Initialize coordinator.""" super().__init__( hass, _LOGGER, - name=f"{username}@SleepIQPause", + config_entry=config_entry, + name=f"{config_entry.data[CONF_USERNAME]}@SleepIQPause", update_interval=LONGER_UPDATE_INTERVAL, ) self.client = client diff --git a/homeassistant/components/slide_local/__init__.py b/homeassistant/components/slide_local/__init__.py index 5b4867bf337..4690fe8016c 100644 --- a/homeassistant/components/slide_local/__init__.py +++ b/homeassistant/components/slide_local/__init__.py @@ -2,14 +2,12 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .coordinator import SlideCoordinator +from .coordinator import SlideConfigEntry, SlideCoordinator PLATFORMS = [Platform.BUTTON, Platform.COVER, Platform.SWITCH] -type SlideConfigEntry = ConfigEntry[SlideCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: SlideConfigEntry) -> bool: diff --git a/homeassistant/components/slide_local/button.py b/homeassistant/components/slide_local/button.py index faca7cb3f2b..12474969ca6 100644 --- a/homeassistant/components/slide_local/button.py +++ b/homeassistant/components/slide_local/button.py @@ -15,9 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SlideConfigEntry from .const import DOMAIN -from .coordinator import SlideCoordinator +from .coordinator import SlideConfigEntry, SlideCoordinator from .entity import SlideEntity PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/slide_local/config_flow.py b/homeassistant/components/slide_local/config_flow.py index 4ceb347568f..96aac1a135c 100644 --- a/homeassistant/components/slide_local/config_flow.py +++ b/homeassistant/components/slide_local/config_flow.py @@ -20,8 +20,8 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from . import SlideConfigEntry from .const import CONF_INVERT_POSITION, DOMAIN +from .coordinator import SlideConfigEntry _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/slide_local/coordinator.py b/homeassistant/components/slide_local/coordinator.py index e5311967198..cbc3e653739 100644 --- a/homeassistant/components/slide_local/coordinator.py +++ b/homeassistant/components/slide_local/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import TYPE_CHECKING, Any +from typing import Any from goslideapi.goslideapi import ( AuthenticationFailed, @@ -14,6 +14,7 @@ from goslideapi.goslideapi import ( GoSlideLocal as SlideLocalApi, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, @@ -31,23 +32,30 @@ from .const import DEFAULT_OFFSET, DOMAIN _LOGGER = logging.getLogger(__name__) -if TYPE_CHECKING: - from . import SlideConfigEntry +type SlideConfigEntry = ConfigEntry[SlideCoordinator] class SlideCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Get and update the latest data.""" - def __init__(self, hass: HomeAssistant, entry: SlideConfigEntry) -> None: + config_entry: SlideConfigEntry + + def __init__(self, hass: HomeAssistant, config_entry: SlideConfigEntry) -> None: """Initialize the data object.""" super().__init__( - hass, _LOGGER, name="Slide", update_interval=timedelta(seconds=15) + hass, + _LOGGER, + config_entry=config_entry, + name="Slide", + update_interval=timedelta(seconds=15), ) self.slide = SlideLocalApi() - self.api_version = entry.data[CONF_API_VERSION] - self.mac = entry.data[CONF_MAC] - self.host = entry.data[CONF_HOST] - self.password = entry.data[CONF_PASSWORD] if self.api_version == 1 else "" + self.api_version = config_entry.data[CONF_API_VERSION] + self.mac = config_entry.data[CONF_MAC] + self.host = config_entry.data[CONF_HOST] + self.password = ( + config_entry.data[CONF_PASSWORD] if self.api_version == 1 else "" + ) async def _async_setup(self) -> None: """Do initialization logic for Slide coordinator.""" diff --git a/homeassistant/components/slide_local/cover.py b/homeassistant/components/slide_local/cover.py index cf04f46d139..0e5e647dea8 100644 --- a/homeassistant/components/slide_local/cover.py +++ b/homeassistant/components/slide_local/cover.py @@ -10,9 +10,8 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SlideConfigEntry from .const import CONF_INVERT_POSITION, DEFAULT_OFFSET -from .coordinator import SlideCoordinator +from .coordinator import SlideConfigEntry, SlideCoordinator from .entity import SlideEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/slide_local/diagnostics.py b/homeassistant/components/slide_local/diagnostics.py index 2655cb5fada..6a70720a14a 100644 --- a/homeassistant/components/slide_local/diagnostics.py +++ b/homeassistant/components/slide_local/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from . import SlideConfigEntry +from .coordinator import SlideConfigEntry TO_REDACT = [ CONF_PASSWORD, diff --git a/homeassistant/components/slide_local/switch.py b/homeassistant/components/slide_local/switch.py index 0471dfcc4e6..8de608b7fc0 100644 --- a/homeassistant/components/slide_local/switch.py +++ b/homeassistant/components/slide_local/switch.py @@ -17,9 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SlideConfigEntry from .const import DOMAIN -from .coordinator import SlideCoordinator +from .coordinator import SlideConfigEntry, SlideCoordinator from .entity import SlideEntity PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index 0e1e99aa444..aab8c6ab3c7 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -89,7 +89,7 @@ async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: async def async_setup_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool: """Set up the Smarty environment from a config entry.""" - coordinator = SmartyCoordinator(hass) + coordinator = SmartyCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/smarty/coordinator.py b/homeassistant/components/smarty/coordinator.py index d7f3e2452d1..a55c9f2e78f 100644 --- a/homeassistant/components/smarty/coordinator.py +++ b/homeassistant/components/smarty/coordinator.py @@ -22,15 +22,16 @@ class SmartyCoordinator(DataUpdateCoordinator[None]): software_version: str configuration_version: str - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_entry: SmartyConfigEntry) -> None: """Initialize.""" super().__init__( hass, logger=_LOGGER, + config_entry=config_entry, name="Smarty", update_interval=timedelta(seconds=30), ) - self.client = Smarty(host=self.config_entry.data[CONF_HOST]) + self.client = Smarty(host=config_entry.data[CONF_HOST]) async def _async_setup(self) -> None: if not await self.hass.async_add_executor_job(self.client.update): diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 2521df3a333..387edfc6e11 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from smhi.smhi_lib import Smhi, SmhiForecastException +from pysmhi import SmhiForecastException, SMHIPointForecast import voluptuous as vol from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN @@ -26,9 +26,9 @@ async def async_check_location( ) -> bool: """Return true if location is ok.""" session = aiohttp_client.async_get_clientsession(hass) - smhi_api = Smhi(longitude, latitude, session=session) + smhi_api = SMHIPointForecast(str(longitude), str(latitude), session=session) try: - await smhi_api.async_get_forecast() + await smhi_api.async_get_daily_forecast() except SmhiForecastException: return False diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 645ace41cab..fc3af634764 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", "iot_class": "cloud_polling", - "loggers": ["smhi"], - "requirements": ["smhi-pkg==1.0.19"] + "loggers": ["pysmhi"], + "requirements": ["pysmhi==1.0.0"] } diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d43ca4465ae..1707afa2fca 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -9,8 +9,7 @@ import logging from typing import Any, Final import aiohttp -from smhi import Smhi -from smhi.smhi_lib import SmhiForecast, SmhiForecastException +from pysmhi import SMHIForecast, SmhiForecastException, SMHIPointForecast from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -59,7 +58,7 @@ from homeassistant.helpers import aiohttp_client, sun from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.util import Throttle, dt as dt_util +from homeassistant.util import Throttle from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT @@ -139,10 +138,10 @@ class SmhiWeather(WeatherEntity): ) -> None: """Initialize the SMHI weather entity.""" self._attr_unique_id = f"{latitude}, {longitude}" - self._forecast_daily: list[SmhiForecast] | None = None - self._forecast_hourly: list[SmhiForecast] | None = None + self._forecast_daily: list[SMHIForecast] | None = None + self._forecast_hourly: list[SMHIForecast] | None = None self._fail_count = 0 - self._smhi_api = Smhi(longitude, latitude, session=session) + self._smhi_api = SMHIPointForecast(longitude, latitude, session=session) self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{latitude}, {longitude}")}, @@ -156,7 +155,7 @@ class SmhiWeather(WeatherEntity): """Return additional attributes.""" if self._forecast_daily: return { - ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0].thunder, + ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0]["thunder"], } return None @@ -165,8 +164,8 @@ class SmhiWeather(WeatherEntity): """Refresh the forecast data from SMHI weather API.""" try: async with asyncio.timeout(TIMEOUT): - self._forecast_daily = await self._smhi_api.async_get_forecast() - self._forecast_hourly = await self._smhi_api.async_get_forecast_hour() + self._forecast_daily = await self._smhi_api.async_get_daily_forecast() + self._forecast_hourly = await self._smhi_api.async_get_hourly_forecast() self._fail_count = 0 except (TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") @@ -176,15 +175,15 @@ class SmhiWeather(WeatherEntity): return if self._forecast_daily: - self._attr_native_temperature = self._forecast_daily[0].temperature - self._attr_humidity = self._forecast_daily[0].humidity - self._attr_native_wind_speed = self._forecast_daily[0].wind_speed - self._attr_wind_bearing = self._forecast_daily[0].wind_direction - self._attr_native_visibility = self._forecast_daily[0].horizontal_visibility - self._attr_native_pressure = self._forecast_daily[0].pressure - self._attr_native_wind_gust_speed = self._forecast_daily[0].wind_gust - self._attr_cloud_coverage = self._forecast_daily[0].cloudiness - self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0].symbol) + self._attr_native_temperature = self._forecast_daily[0]["temperature"] + self._attr_humidity = self._forecast_daily[0]["humidity"] + self._attr_native_wind_speed = self._forecast_daily[0]["wind_speed"] + self._attr_wind_bearing = self._forecast_daily[0]["wind_direction"] + self._attr_native_visibility = self._forecast_daily[0]["visibility"] + self._attr_native_pressure = self._forecast_daily[0]["pressure"] + self._attr_native_wind_gust_speed = self._forecast_daily[0]["wind_gust"] + self._attr_cloud_coverage = self._forecast_daily[0]["total_cloud"] + self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0]["symbol"]) if self._attr_condition == ATTR_CONDITION_SUNNY and not sun.is_up( self.hass ): @@ -196,7 +195,7 @@ class SmhiWeather(WeatherEntity): await self.async_update(no_throttle=True) def _get_forecast_data( - self, forecast_data: list[SmhiForecast] | None + self, forecast_data: list[SMHIForecast] | None ) -> list[Forecast] | None: """Get forecast data.""" if forecast_data is None or len(forecast_data) < 3: @@ -205,25 +204,28 @@ class SmhiWeather(WeatherEntity): data: list[Forecast] = [] for forecast in forecast_data[1:]: - condition = CONDITION_MAP.get(forecast.symbol) + condition = CONDITION_MAP.get(forecast["symbol"]) if condition == ATTR_CONDITION_SUNNY and not sun.is_up( - self.hass, forecast.valid_time.replace(tzinfo=dt_util.UTC) + self.hass, forecast["valid_time"] ): condition = ATTR_CONDITION_CLEAR_NIGHT data.append( { - ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), - ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, - ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, - ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, + ATTR_FORECAST_TIME: forecast["valid_time"].isoformat(), + ATTR_FORECAST_NATIVE_TEMP: forecast["temperature_max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["temperature_min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.get( + "total_precipitation" + ) + or forecast["mean_precipitation"], ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure, - ATTR_FORECAST_WIND_BEARING: forecast.wind_direction, - ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed, - ATTR_FORECAST_HUMIDITY: forecast.humidity, - ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust, - ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness, + ATTR_FORECAST_NATIVE_PRESSURE: forecast["pressure"], + ATTR_FORECAST_WIND_BEARING: forecast["wind_direction"], + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind_speed"], + ATTR_FORECAST_HUMIDITY: forecast["humidity"], + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast["wind_gust"], + ATTR_FORECAST_CLOUD_COVERAGE: forecast["total_cloud"], } ) diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index d37cf355fce..44f015eedeb 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import date, datetime, timedelta -from typing import Any +from typing import TYPE_CHECKING, Any from aiosolaredge import SolarEdge from stringcase import snakecase @@ -21,13 +21,22 @@ from .const import ( POWER_FLOW_UPDATE_DELAY, ) +if TYPE_CHECKING: + from .types import SolarEdgeConfigEntry + class SolarEdgeDataService(ABC): """Get and update the latest data.""" coordinator: DataUpdateCoordinator[None] - def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: SolarEdgeConfigEntry, + api: SolarEdge, + site_id: str, + ) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id @@ -36,6 +45,7 @@ class SolarEdgeDataService(ABC): self.attributes: dict[str, Any] = {} self.hass = hass + self.config_entry = config_entry @callback def async_setup(self) -> None: @@ -43,6 +53,7 @@ class SolarEdgeDataService(ABC): self.coordinator = DataUpdateCoordinator( self.hass, LOGGER, + config_entry=self.config_entry, name=str(self), update_method=self.async_update_data, update_interval=self.update_interval, @@ -174,9 +185,15 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: SolarEdgeConfigEntry, + api: SolarEdge, + site_id: str, + ) -> None: """Initialize the power flow data service.""" - super().__init__(hass, api, site_id) + super().__init__(hass, config_entry, api, site_id) self.unit = None @@ -234,9 +251,15 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: SolarEdgeConfigEntry, + api: SolarEdge, + site_id: str, + ) -> None: """Initialize the power flow data service.""" - super().__init__(hass, api, site_id) + super().__init__(hass, config_entry, api, site_id) self.unit = None diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 4b2398d15c2..004335b644b 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -206,7 +206,7 @@ async def async_setup_entry( """Add an solarEdge entry.""" # Add the needed sensors to hass api = entry.runtime_data[DATA_API_CLIENT] - sensor_factory = SolarEdgeSensorFactory(hass, entry.data[CONF_SITE_ID], api) + sensor_factory = SolarEdgeSensorFactory(hass, entry, entry.data[CONF_SITE_ID], api) for service in sensor_factory.all_services: service.async_setup() await service.coordinator.async_refresh() @@ -222,14 +222,20 @@ async def async_setup_entry( class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, hass: HomeAssistant, site_id: str, api: SolarEdge) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: SolarEdgeConfigEntry, + site_id: str, + api: SolarEdge, + ) -> None: """Initialize the factory.""" - details = SolarEdgeDetailsDataService(hass, api, site_id) - overview = SolarEdgeOverviewDataService(hass, api, site_id) - inventory = SolarEdgeInventoryDataService(hass, api, site_id) - flow = SolarEdgePowerFlowDataService(hass, api, site_id) - energy = SolarEdgeEnergyDetailsService(hass, api, site_id) + details = SolarEdgeDetailsDataService(hass, config_entry, api, site_id) + overview = SolarEdgeOverviewDataService(hass, config_entry, api, site_id) + inventory = SolarEdgeInventoryDataService(hass, config_entry, api, site_id) + flow = SolarEdgePowerFlowDataService(hass, config_entry, api, site_id) + energy = SolarEdgeEnergyDetailsService(hass, config_entry, api, site_id) self.all_services = (details, overview, inventory, flow, energy) 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/sql/strings.json b/homeassistant/components/sql/strings.json index cd36ccf7731..ac861e72b72 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -5,7 +5,7 @@ }, "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", + "query_invalid": "SQL query invalid", "query_no_read_only": "SQL query must be read-only", "multiple_queries": "Multiple SQL queries are not supported", "column_invalid": "The column `{column}` is not returned by the query" @@ -15,22 +15,22 @@ "data": { "db_url": "Database URL", "name": "[%key:common::config_flow::data::name%]", - "query": "Select Query", + "query": "Select query", "column": "Column", - "unit_of_measurement": "Unit of Measure", - "value_template": "Value Template", - "device_class": "Device Class", - "state_class": "State Class" + "unit_of_measurement": "Unit of measurement", + "value_template": "Value template", + "device_class": "Device class", + "state_class": "State class" }, "data_description": { - "db_url": "Database URL, leave empty to use HA recorder database", - "name": "Name that will be used for Config Entry and also the Sensor", + "db_url": "Leave empty to use Home Assistant Recorder database", + "name": "Name that will be used for config entry and also the sensor", "query": "Query to run, needs to start with 'SELECT'", "column": "Column for returned query to present as state", - "unit_of_measurement": "Unit of Measure (optional)", - "value_template": "Value Template (optional)", + "unit_of_measurement": "The unit of measurement for the sensor (optional)", + "value_template": "Template to extract a value from the payload (optional)", "device_class": "The type/class of the sensor to set the icon in the frontend", - "state_class": "The state_class of the sensor" + "state_class": "The state class of the sensor" } } } diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index 6e45758fb94..7a2c32cb4d5 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -2,19 +2,17 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .coordinator import SteamDataUpdateCoordinator +from .coordinator import SteamConfigEntry, SteamDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -type SteamConfigEntry = ConfigEntry[SteamDataUpdateCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> bool: """Set up Steam from a config entry.""" - coordinator = SteamDataUpdateCoordinator(hass) + coordinator = SteamDataUpdateCoordinator(hass, entry) 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/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 69009fca8c4..57c75f0a704 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -18,8 +18,8 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import SteamConfigEntry from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS +from .coordinator import SteamConfigEntry # To avoid too long request URIs, the amount of ids to request is limited MAX_IDS_TO_REQUEST = 275 diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 81a3bb0d898..731154183ed 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -3,11 +3,11 @@ from __future__ import annotations from datetime import timedelta -from typing import TYPE_CHECKING import steam from steam.api import _interface_method as INTMethod +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -15,8 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_ACCOUNTS, DOMAIN, LOGGER -if TYPE_CHECKING: - from . import SteamConfigEntry +type SteamConfigEntry = ConfigEntry[SteamDataUpdateCoordinator] class SteamDataUpdateCoordinator( @@ -26,11 +25,12 @@ class SteamDataUpdateCoordinator( config_entry: SteamConfigEntry - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_entry: SteamConfigEntry) -> None: """Initialize the coordinator.""" super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=30), ) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 058bb386383..625a8b95979 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -12,7 +12,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp -from . import SteamConfigEntry from .const import ( CONF_ACCOUNTS, STEAM_API_URL, @@ -21,7 +20,7 @@ from .const import ( STEAM_MAIN_IMAGE_FILE, STEAM_STATUSES, ) -from .coordinator import SteamDataUpdateCoordinator +from .coordinator import SteamConfigEntry, SteamDataUpdateCoordinator from .entity import SteamEntity PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/steamist/__init__.py b/homeassistant/components/steamist/__init__.py index 8d8401ec6fd..380f25ea8da 100644 --- a/homeassistant/components/steamist/__init__.py +++ b/homeassistant/components/steamist/__init__.py @@ -8,7 +8,7 @@ from typing import Any from aiosteamist import Steamist from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, Platform +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -52,9 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] coordinator = SteamistDataUpdateCoordinator( hass, + entry, Steamist(host, async_get_clientsession(hass)), - host, - entry.data.get(CONF_NAME), # Only found from discovery ) await coordinator.async_config_entry_first_refresh() if not async_get_discovery(hass, host): diff --git a/homeassistant/components/steamist/coordinator.py b/homeassistant/components/steamist/coordinator.py index c5aa7be7ddc..3f864364be7 100644 --- a/homeassistant/components/steamist/coordinator.py +++ b/homeassistant/components/steamist/coordinator.py @@ -7,6 +7,8 @@ import logging from aiosteamist import Steamist, SteamistStatus +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -16,20 +18,22 @@ _LOGGER = logging.getLogger(__name__) class SteamistDataUpdateCoordinator(DataUpdateCoordinator[SteamistStatus]): """DataUpdateCoordinator to gather data from a steamist steam shower.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: Steamist, - host: str, - device_name: str | None, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific steamist.""" self.client = client - self.device_name = device_name + self.device_name = config_entry.data.get(CONF_NAME) # Only found from discovery super().__init__( hass, _LOGGER, - name=f"Steamist {host}", + config_entry=config_entry, + name=f"Steamist {config_entry.data[CONF_HOST]}", update_interval=timedelta(seconds=5), always_update=False, ) diff --git a/homeassistant/components/stookwijzer/coordinator.py b/homeassistant/components/stookwijzer/coordinator.py index 23092bed66e..8f81494b7d5 100644 --- a/homeassistant/components/stookwijzer/coordinator.py +++ b/homeassistant/components/stookwijzer/coordinator.py @@ -20,18 +20,23 @@ type StookwijzerConfigEntry = ConfigEntry[StookwijzerCoordinator] class StookwijzerCoordinator(DataUpdateCoordinator[None]): """Stookwijzer update coordinator.""" - def __init__(self, hass: HomeAssistant, entry: StookwijzerConfigEntry) -> None: + config_entry: StookwijzerConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: StookwijzerConfigEntry + ) -> None: """Initialize the coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) self.client = Stookwijzer( async_get_clientsession(hass), - entry.data[CONF_LATITUDE], - entry.data[CONF_LONGITUDE], + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], ) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 313fc1f24c5..1c1357a9b2b 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key = entry.data[CONF_API_KEY] client = StreamlabsClient(api_key) - coordinator = StreamlabsCoordinator(hass, client) + coordinator = StreamlabsCoordinator(hass, entry, client) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/streamlabswater/coordinator.py b/homeassistant/components/streamlabswater/coordinator.py index 56e67abe222..df4a6056b36 100644 --- a/homeassistant/components/streamlabswater/coordinator.py +++ b/homeassistant/components/streamlabswater/coordinator.py @@ -5,6 +5,7 @@ from datetime import timedelta from streamlabswater.streamlabswater import StreamlabsClient +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -25,15 +26,19 @@ class StreamlabsData: class StreamlabsCoordinator(DataUpdateCoordinator[dict[str, StreamlabsData]]): """Coordinator for Streamlabs.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: StreamlabsClient, ) -> None: """Coordinator for Streamlabs.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name="Streamlabs", update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index e1f846d63a7..130242b7742 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -38,8 +38,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: hass.data[DOMAIN][entry.entry_id] = coordinator = SurePetcareDataCoordinator( - entry, hass, + entry, ) except SurePetcareAuthenticationError as error: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") diff --git a/homeassistant/components/surepetcare/coordinator.py b/homeassistant/components/surepetcare/coordinator.py index a80e96ad185..d8112cebc90 100644 --- a/homeassistant/components/surepetcare/coordinator.py +++ b/homeassistant/components/surepetcare/coordinator.py @@ -33,7 +33,9 @@ SCAN_INTERVAL = timedelta(minutes=3) class SurePetcareDataCoordinator(DataUpdateCoordinator[dict[int, SurepyEntity]]): """Handle Surepetcare data.""" - def __init__(self, entry: ConfigEntry, hass: HomeAssistant) -> None: + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the data handler.""" self.surepy = Surepy( entry.data[CONF_USERNAME], @@ -51,6 +53,7 @@ class SurePetcareDataCoordinator(DataUpdateCoordinator[dict[int, SurepyEntity]]) super().__init__( hass, _LOGGER, + config_entry=entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 628f6e95c2a..0d0c4dc6169 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -96,7 +96,9 @@ async def async_setup_entry( }, ) from e - coordinator = SwissPublicTransportDataUpdateCoordinator(hass, opendata, time_offset) + coordinator = SwissPublicTransportDataUpdateCoordinator( + hass, entry, opendata, time_offset + ) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index 81322117a6f..32b52122c7d 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -61,6 +61,7 @@ class SwissPublicTransportDataUpdateCoordinator( def __init__( self, hass: HomeAssistant, + config_entry: SwissPublicTransportConfigEntry, opendata: OpendataTransport, time_offset: dict[str, int] | None, ) -> None: @@ -68,6 +69,7 @@ class SwissPublicTransportDataUpdateCoordinator( super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=DEFAULT_UPDATE_TIME), ) diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index a2a3ecf0df9..6e4bf004a3d 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -63,10 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = async_get_clientsession(hass, verify_ssl=False) api = await get_api_object(central_unit, user, password, websession) - coordinator = SwitchBeeCoordinator( - hass, - api, - ) + coordinator = SwitchBeeCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index 49400e3c28d..b0ea1707be8 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -10,6 +10,7 @@ from switchbee.api import CentralUnitPolling, CentralUnitWsRPC from switchbee.api.central_unit import SwitchBeeError from switchbee.device import DeviceType, SwitchBeeBaseDevice +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -22,9 +23,12 @@ _LOGGER = logging.getLogger(__name__) class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevice]]): """Class to manage fetching SwitchBee data API.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, swb_api: CentralUnitPolling | CentralUnitWsRPC, ) -> None: """Initialize.""" @@ -39,6 +43,7 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=SCAN_INTERVAL_SEC[type(self.api)]), ) 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/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 7d2224fc6fc..37e9ee3d929 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -220,7 +220,7 @@ async def handle_info( # Update subscription of all finished tasks for result in done: domain, key = pending_lookup[result] - event_msg = { + event_msg: dict[str, Any] = { "type": "update", "domain": domain, "key": key, diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 3e42e33489f..4087183bfe5 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -87,7 +87,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool @callback -def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: TadoConfigEntry +): options = dict(entry.options) if CONF_FALLBACK not in options: options[CONF_FALLBACK] = entry.data.get( diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index ddec9e7f292..6e932c8ccfc 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -4,18 +4,20 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from PyTado.interface import Tado from requests import RequestException from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +if TYPE_CHECKING: + from . import TadoConfigEntry + from .const import ( CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT, @@ -31,8 +33,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30) -type TadoConfigEntry = ConfigEntry[TadoDataUpdateCoordinator] - class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage API calls from and to Tado via PyTado.""" @@ -45,7 +45,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: TadoConfigEntry, tado: Tado, debug: bool = False, ) -> None: @@ -53,13 +53,16 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_INTERVAL, ) self._tado = tado - self._username = entry.data[CONF_USERNAME] - self._password = entry.data[CONF_PASSWORD] - self._fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT) + self._username = config_entry.data[CONF_USERNAME] + self._password = config_entry.data[CONF_PASSWORD] + self._fallback = config_entry.options.get( + CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT + ) self._debug = debug self.home_id: int @@ -343,16 +346,19 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage the mobile devices from Tado via PyTado.""" + config_entry: TadoConfigEntry + def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: TadoConfigEntry, tado: Tado, ) -> None: """Initialize the Tado data update coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=SCAN_MOBILE_DEVICE_INTERVAL, ) 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/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index f744265e1c2..fa3ec1dc4f7 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -175,6 +175,7 @@ BASE_SERVICE_SCHEMA = vol.Schema( vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, vol.Optional(ATTR_TIMEOUT): cv.positive_int, vol.Optional(ATTR_MESSAGE_TAG): cv.string, + vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int), }, extra=vol.ALLOW_EXTRA, ) @@ -216,6 +217,7 @@ SERVICE_SCHEMA_SEND_POLL = vol.Schema( vol.Optional(ATTR_ALLOWS_MULTIPLE_ANSWERS, default=False): cv.boolean, vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean, vol.Optional(ATTR_TIMEOUT): cv.positive_int, + vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int), } ) 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/toon/__init__.py b/homeassistant/components/toon/__init__.py index 43c787b2301..1c56b780c0f 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -89,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) - coordinator = ToonDataUpdateCoordinator(hass, entry=entry, session=session) + coordinator = ToonDataUpdateCoordinator(hass, entry, session) await coordinator.toon.activate_agreement( agreement_id=entry.data[CONF_AGREEMENT_ID] ) diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 586eca34959..894b4c91334 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -28,12 +28,13 @@ _LOGGER = logging.getLogger(__name__) class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): """Class to manage fetching Toon data from single endpoint.""" + config_entry: ConfigEntry + def __init__( - self, hass: HomeAssistant, *, entry: ConfigEntry, session: OAuth2Session + self, hass: HomeAssistant, entry: ConfigEntry, session: OAuth2Session ) -> None: """Initialize global Toon data updater.""" self.session = session - self.entry = entry async def async_token_refresh() -> str: await session.async_ensure_token_valid() @@ -46,49 +47,55 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): ) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + hass, + _LOGGER, + config_entry=entry, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, ) async def register_webhook(self, event: Event | None = None) -> None: """Register a webhook with Toon to get live updates.""" - if CONF_WEBHOOK_ID not in self.entry.data: - data = {**self.entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} - self.hass.config_entries.async_update_entry(self.entry, data=data) + if CONF_WEBHOOK_ID not in self.config_entry.data: + data = {**self.config_entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} + self.hass.config_entries.async_update_entry(self.config_entry, data=data) if cloud.async_active_subscription(self.hass): - if CONF_CLOUDHOOK_URL not in self.entry.data: + if CONF_CLOUDHOOK_URL not in self.config_entry.data: try: webhook_url = await cloud.async_create_cloudhook( - self.hass, self.entry.data[CONF_WEBHOOK_ID] + self.hass, self.config_entry.data[CONF_WEBHOOK_ID] ) except cloud.CloudNotConnected: webhook_url = webhook.async_generate_url( - self.hass, self.entry.data[CONF_WEBHOOK_ID] + self.hass, self.config_entry.data[CONF_WEBHOOK_ID] ) else: - data = {**self.entry.data, CONF_CLOUDHOOK_URL: webhook_url} - self.hass.config_entries.async_update_entry(self.entry, data=data) + data = {**self.config_entry.data, CONF_CLOUDHOOK_URL: webhook_url} + self.hass.config_entries.async_update_entry( + self.config_entry, data=data + ) else: - webhook_url = self.entry.data[CONF_CLOUDHOOK_URL] + webhook_url = self.config_entry.data[CONF_CLOUDHOOK_URL] else: webhook_url = webhook.async_generate_url( - self.hass, self.entry.data[CONF_WEBHOOK_ID] + self.hass, self.config_entry.data[CONF_WEBHOOK_ID] ) # Ensure the webhook is not registered already - webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + webhook_unregister(self.hass, self.config_entry.data[CONF_WEBHOOK_ID]) webhook_register( self.hass, DOMAIN, "Toon", - self.entry.data[CONF_WEBHOOK_ID], + self.config_entry.data[CONF_WEBHOOK_ID], self.handle_webhook, ) try: await self.toon.subscribe_webhook( - application_id=self.entry.entry_id, url=webhook_url + application_id=self.config_entry.entry_id, url=webhook_url ) _LOGGER.debug("Registered Toon webhook: %s", webhook_url) except ToonError as err: @@ -131,14 +138,14 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): async def unregister_webhook(self, event: Event | None = None) -> None: """Remove / Unregister webhook for toon.""" _LOGGER.debug( - "Unregistering Toon webhook (%s)", self.entry.data[CONF_WEBHOOK_ID] + "Unregistering Toon webhook (%s)", self.config_entry.data[CONF_WEBHOOK_ID] ) try: - await self.toon.unsubscribe_webhook(self.entry.entry_id) + await self.toon.unsubscribe_webhook(self.config_entry.entry_id) except ToonError as err: _LOGGER.error("Failed unregistering Toon webhook - %s", err) - webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + webhook_unregister(self.hass, self.config_entry.data[CONF_WEBHOOK_ID]) async def _async_update_data(self) -> Status: """Fetch data from Toon.""" diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 9ca2fe80cf9..291a7e78c62 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -328,7 +328,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): host, port = self._async_get_host_port(host) - match_dict = {CONF_HOST: host} + match_dict: dict[str, Any] = {CONF_HOST: host} if port: self.port = port match_dict[CONF_PORT] = port diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index d1b4694779d..fcd1335a77a 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -46,9 +46,11 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): device: Device, update_interval: timedelta, config_entry: TPLinkConfigEntry, + parent_coordinator: TPLinkDataUpdateCoordinator | None = None, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" self.device = device + self.parent_coordinator = parent_coordinator # The iot HS300 allows a limited number of concurrent requests and # fetching the emeter information requires separate ones, so child @@ -95,6 +97,12 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): ) from ex await self._process_child_devices() + if not self._update_children: + # If the children are not being updated, it means this is an + # IotStrip, and we need to tell the children to write state + # since the power state is provided by the parent. + for child_coordinator in self._child_coordinators.values(): + child_coordinator.async_set_updated_data(None) async def _process_child_devices(self) -> None: """Process child devices and remove stale devices.""" @@ -132,7 +140,11 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): # The child coordinators only update energy data so we can # set a longer update interval to avoid flooding the device child_coordinator = TPLinkDataUpdateCoordinator( - self.hass, child, timedelta(seconds=60), self.config_entry + self.hass, + child, + timedelta(seconds=60), + self.config_entry, + parent_coordinator=self, ) self._child_coordinators[child.device_id] = child_coordinator return child_coordinator diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 15c07655e69..7a0d811b30d 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -151,7 +151,13 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( "exc": str(ex), }, ) from ex - await self.coordinator.async_request_refresh() + coordinator = self.coordinator + if coordinator.parent_coordinator: + # If there is a parent coordinator we need to refresh + # the parent as its what provides the power state data + # for the child entities. + coordinator = coordinator.parent_coordinator + await coordinator.async_request_refresh() return _async_wrap diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 1a8ffdea0c2..578488dad1a 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -78,7 +78,9 @@ MIGRATION_NAME_TO_KEY = { SERVICE_BASE_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(), + vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector( + {"integration": DOMAIN} + ), } ) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index aacb7538b61..757cad221b5 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -8,7 +8,6 @@ from datetime import timedelta from async_upnp_client.exceptions import UpnpConnectionError from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -28,7 +27,7 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, LOGGER, ) -from .coordinator import UpnpDataUpdateCoordinator +from .coordinator import UpnpConfigEntry, UpnpDataUpdateCoordinator from .device import async_create_device, get_preferred_location NOTIFICATION_ID = "upnp_notification" @@ -37,9 +36,6 @@ NOTIFICATION_TITLE = "UPnP/IGD Setup" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -type UpnpConfigEntry = ConfigEntry[UpnpDataUpdateCoordinator] - - async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" LOGGER.debug("Setting up config entry: %s", entry.entry_id) @@ -176,6 +172,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL) coordinator = UpnpDataUpdateCoordinator( hass, + config_entry=entry, device=device, device_entry=device_entry, update_interval=update_interval, @@ -193,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool: """Unload a UPnP/IGD device from a config entry.""" LOGGER.debug("Unloading config entry: %s", entry.entry_id) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index fb32946bf7d..1576cccac6a 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -13,8 +13,8 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import UpnpConfigEntry, UpnpDataUpdateCoordinator from .const import LOGGER, WAN_STATUS +from .coordinator import UpnpConfigEntry, UpnpDataUpdateCoordinator from .entity import UpnpEntity, UpnpEntityDescription diff --git a/homeassistant/components/upnp/coordinator.py b/homeassistant/components/upnp/coordinator.py index 37ff700bfe2..03e4c53f143 100644 --- a/homeassistant/components/upnp/coordinator.py +++ b/homeassistant/components/upnp/coordinator.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from async_upnp_client.exceptions import UpnpCommunicationError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -13,15 +14,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import LOGGER from .device import Device +type UpnpConfigEntry = ConfigEntry[UpnpDataUpdateCoordinator] + class UpnpDataUpdateCoordinator( DataUpdateCoordinator[dict[str, str | datetime | int | float | None]] ): """Define an object to update data from UPNP device.""" + config_entry: UpnpConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: UpnpConfigEntry, device: Device, device_entry: DeviceEntry, update_interval: timedelta, @@ -34,6 +40,7 @@ class UpnpDataUpdateCoordinator( super().__init__( hass, LOGGER, + config_entry=config_entry, name=device.name, update_interval=update_interval, ) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index aae2f8308c1..c0e77315f77 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import UpnpConfigEntry from .const import ( BYTES_RECEIVED, BYTES_SENT, @@ -38,6 +37,7 @@ from .const import ( ROUTER_UPTIME, WAN_STATUS, ) +from .coordinator import UpnpConfigEntry from .entity import UpnpEntity, UpnpEntityDescription diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index afff0c8fe03..b8619b1fe39 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, PLATFORMS @@ -24,12 +23,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Wrong API key type detected, use the 'main' API key" ) uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass)) - dev_reg = dr.async_get(hass) hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( hass, - config_entry_id=entry.entry_id, - dev_reg=dev_reg, + entry, api=uptime_robot_api, ) diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 3069884eb99..fbadc237965 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -26,19 +26,18 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon def __init__( self, hass: HomeAssistant, - config_entry_id: str, - dev_reg: dr.DeviceRegistry, + config_entry: ConfigEntry, api: UptimeRobot, ) -> None: """Initialize coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=COORDINATOR_UPDATE_INTERVAL, ) - self._config_entry_id = config_entry_id - self._device_registry = dev_reg + self._device_registry = dr.async_get(hass) self.api = api async def _async_update_data(self) -> list[UptimeRobotMonitor]: @@ -58,7 +57,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon current_monitors = { list(device.identifiers)[0][1] for device in dr.async_entries_for_config_entry( - self._device_registry, self._config_entry_id + self._device_registry, self.config_entry.entry_id ) } new_monitors = {str(monitor.id) for monitor in monitors} @@ -73,7 +72,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon # create new devices and entities. if self.data and new_monitors - {str(monitor.id) for monitor in self.data}: self.hass.async_create_task( - self.hass.config_entries.async_reload(self._config_entry_id) + self.hass.config_entries.async_reload(self.config_entry.entry_id) ) return monitors diff --git a/homeassistant/components/v2c/__init__.py b/homeassistant/components/v2c/__init__.py index 0c07891df72..7cd5e71f3ae 100644 --- a/homeassistant/components/v2c/__init__.py +++ b/homeassistant/components/v2c/__init__.py @@ -4,12 +4,11 @@ from __future__ import annotations from pytrydan import Trydan -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client -from .coordinator import V2CUpdateCoordinator +from .coordinator import V2CConfigEntry, V2CUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -19,15 +18,11 @@ PLATFORMS: list[Platform] = [ ] -type V2CConfigEntry = ConfigEntry[V2CUpdateCoordinator] - - async def async_setup_entry(hass: HomeAssistant, entry: V2CConfigEntry) -> bool: """Set up V2C from a config entry.""" - host = entry.data[CONF_HOST] - trydan = Trydan(host, get_async_client(hass, verify_ssl=False)) - coordinator = V2CUpdateCoordinator(hass, trydan, host) + trydan = Trydan(entry.data[CONF_HOST], get_async_client(hass, verify_ssl=False)) + coordinator = V2CUpdateCoordinator(hass, entry, trydan) await coordinator.async_config_entry_first_refresh() @@ -41,6 +36,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: V2CConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: V2CConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/v2c/binary_sensor.py b/homeassistant/components/v2c/binary_sensor.py index 28ad3665996..18724a4eada 100644 --- a/homeassistant/components/v2c/binary_sensor.py +++ b/homeassistant/components/v2c/binary_sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import V2CConfigEntry -from .coordinator import V2CUpdateCoordinator +from .coordinator import V2CConfigEntry, V2CUpdateCoordinator from .entity import V2CBaseEntity diff --git a/homeassistant/components/v2c/coordinator.py b/homeassistant/components/v2c/coordinator.py index b121c84563c..de8015985f9 100644 --- a/homeassistant/components/v2c/coordinator.py +++ b/homeassistant/components/v2c/coordinator.py @@ -9,6 +9,7 @@ from pytrydan import Trydan, TrydanData from pytrydan.exceptions import TrydanError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -16,19 +17,24 @@ SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +type V2CConfigEntry = ConfigEntry[V2CUpdateCoordinator] + class V2CUpdateCoordinator(DataUpdateCoordinator[TrydanData]): """DataUpdateCoordinator to gather data from any v2c.""" - config_entry: ConfigEntry + config_entry: V2CConfigEntry - def __init__(self, hass: HomeAssistant, evse: Trydan, host: str) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: V2CConfigEntry, evse: Trydan + ) -> None: """Initialize DataUpdateCoordinator for a v2c evse.""" self.evse = evse super().__init__( hass, _LOGGER, - name=f"EVSE {host}", + config_entry=config_entry, + name=f"EVSE {config_entry.data[CONF_HOST]}", update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/v2c/diagnostics.py b/homeassistant/components/v2c/diagnostics.py index 289d585b164..994f702a7bd 100644 --- a/homeassistant/components/v2c/diagnostics.py +++ b/homeassistant/components/v2c/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from . import V2CConfigEntry +from .coordinator import V2CConfigEntry TO_REDACT = {CONF_HOST, "title"} diff --git a/homeassistant/components/v2c/number.py b/homeassistant/components/v2c/number.py index 1540b098cf1..0d6401d194f 100644 --- a/homeassistant/components/v2c/number.py +++ b/homeassistant/components/v2c/number.py @@ -17,8 +17,7 @@ from homeassistant.const import EntityCategory, UnitOfElectricCurrent from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import V2CConfigEntry -from .coordinator import V2CUpdateCoordinator +from .coordinator import V2CConfigEntry, V2CUpdateCoordinator from .entity import V2CBaseEntity MIN_INTENSITY = 6 diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 97853740e9d..5b02928385b 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -26,8 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import V2CConfigEntry -from .coordinator import V2CUpdateCoordinator +from .coordinator import V2CConfigEntry, V2CUpdateCoordinator from .entity import V2CBaseEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/v2c/switch.py b/homeassistant/components/v2c/switch.py index cca7da70e48..d6ba6a3b13e 100644 --- a/homeassistant/components/v2c/switch.py +++ b/homeassistant/components/v2c/switch.py @@ -20,8 +20,7 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import V2CConfigEntry -from .coordinator import V2CUpdateCoordinator +from .coordinator import V2CConfigEntry, V2CUpdateCoordinator from .entity import V2CBaseEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 5f34b587163..2b9ae7b60b6 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -49,14 +49,14 @@ class VerisureAlarm( name="Verisure Alarm", manufacturer="Verisure", model="VBox", - identifiers={(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + identifiers={(DOMAIN, self.coordinator.config_entry.data[CONF_GIID])}, configuration_url="https://mypages.verisure.com", ) @property def unique_id(self) -> str: """Return the unique ID for this entity.""" - return self.coordinator.entry.data[CONF_GIID] + return self.coordinator.config_entry.data[CONF_GIID] async def _async_set_arm_state( self, state: str, command_data: dict[str, str | dict[str, str]] diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 542ee3485ce..94a44550d47 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -62,7 +62,7 @@ class VerisureDoorWindowSensor( manufacturer="Verisure", model="Shock Sensor Detector", identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) @@ -104,7 +104,7 @@ class VerisureEthernetStatus( @property def unique_id(self) -> str: """Return the unique ID for this entity.""" - return f"{self.coordinator.entry.data[CONF_GIID]}_ethernet" + return f"{self.coordinator.config_entry.data[CONF_GIID]}_ethernet" @property def device_info(self) -> DeviceInfo: @@ -113,7 +113,7 @@ class VerisureEthernetStatus( name="Verisure Alarm", manufacturer="Verisure", model="VBox", - identifiers={(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + identifiers={(DOMAIN, self.coordinator.config_entry.data[CONF_GIID])}, configuration_url="https://mypages.verisure.com", ) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 70cd436d24c..7f49f917d83 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -75,7 +75,7 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) manufacturer="Verisure", model="SmartCam", identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 930d862257b..5165ddc6d3d 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -25,10 +25,11 @@ from .const import CONF_GIID, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER class VerisureDataUpdateCoordinator(DataUpdateCoordinator): """A Verisure Data Update Coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the Verisure hub.""" self.imageseries: list[dict[str, str]] = [] - self.entry = entry self._overview: list[dict] = [] self.verisure = Verisure( @@ -40,7 +41,11 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): ) super().__init__( - hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + hass, + LOGGER, + config_entry=entry, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, ) async def async_login(self) -> bool: @@ -55,7 +60,7 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): return False await self.hass.async_add_executor_job( - self.verisure.set_giid, self.entry.data[CONF_GIID] + self.verisure.set_giid, self.config_entry.data[CONF_GIID] ) return True diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 87f5c53880e..16c69ecf2e2 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -81,7 +81,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt manufacturer="Verisure", model="Lockguard Smartlock", identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) @@ -109,7 +109,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt @property def code_format(self) -> str: """Return the configured code format.""" - digits = self.coordinator.entry.options.get( + digits = self.coordinator.config_entry.options.get( CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS ) return f"^\\d{{{digits}}}$" diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 4f6e6b3d3c5..77a576caad8 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -72,7 +72,7 @@ class VerisureThermometer( manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) @@ -122,7 +122,7 @@ class VerisureHygrometer( manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index e0238097e01..838e0222087 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -57,7 +57,7 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch manufacturer="Verisure", model="SmartPlug", identifiers={(DOMAIN, self.serial_number)}, - via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + via_device=(DOMAIN, self.coordinator.config_entry.data[CONF_GIID]), configuration_url="https://mypages.verisure.com", ) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 1c55d932425..4951bdb2dc1 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -52,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN] = {} hass.data[DOMAIN][VS_MANAGER] = manager - coordinator = VeSyncDataCoordinator(hass, manager) + coordinator = VeSyncDataCoordinator(hass, config_entry, manager) # Store coordinator at domain level since only single integration instance is permitted. hass.data[DOMAIN][VS_COORDINATOR] = coordinator diff --git a/homeassistant/components/vesync/coordinator.py b/homeassistant/components/vesync/coordinator.py index f3df2970fdb..e8c8396bfb4 100644 --- a/homeassistant/components/vesync/coordinator.py +++ b/homeassistant/components/vesync/coordinator.py @@ -7,6 +7,7 @@ import logging from pyvesync import VeSync +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -18,13 +19,18 @@ _LOGGER = logging.getLogger(__name__) class VeSyncDataCoordinator(DataUpdateCoordinator[None]): """Class representing data coordinator for VeSync devices.""" - def __init__(self, hass: HomeAssistant, manager: VeSync) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync + ) -> None: """Initialize.""" self._manager = manager super().__init__( hass, _LOGGER, + config_entry=config_entry, name="VeSyncDataCoordinator", update_interval=timedelta(seconds=UPDATE_INTERVAL), ) diff --git a/homeassistant/components/vicare/fan.py b/homeassistant/components/vicare/fan.py index 10983a7ad24..c5e24f46c33 100644 --- a/homeassistant/components/vicare/fan.py +++ b/homeassistant/components/vicare/fan.py @@ -196,6 +196,9 @@ class ViCareFan(ViCareEntity, FanEntity): @property def is_on(self) -> bool | None: """Return true if the entity is on.""" + if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): + return False + return self.percentage is not None and self.percentage > 0 def turn_off(self, **kwargs: Any) -> None: @@ -206,6 +209,8 @@ class ViCareFan(ViCareEntity, FanEntity): @property def icon(self) -> str | None: """Return the icon to use in the frontend.""" + if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY): + return "mdi:fan-off" if hasattr(self, "_attr_preset_mode"): if self._attr_preset_mode == VentilationMode.VENTILATION: return "mdi:fan-clock" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 766cf22cb94..489d4accb8a 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "iot_class": "cloud_polling", "loggers": ["PyViCare"], - "requirements": ["PyViCare==2.41.0"] + "requirements": ["PyViCare==2.42.0"] } diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 4af42d76b62..27a7fa2cd97 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV ): store: Store[list[dict[str, Any]]] = Store(hass, 1, DOMAIN) - coordinator = VizioAppsDataUpdateCoordinator(hass, store) + coordinator = VizioAppsDataUpdateCoordinator(hass, entry, store) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator diff --git a/homeassistant/components/vizio/coordinator.py b/homeassistant/components/vizio/coordinator.py index a7ca7d7f9ed..0f95c8a53b7 100644 --- a/homeassistant/components/vizio/coordinator.py +++ b/homeassistant/components/vizio/coordinator.py @@ -9,6 +9,7 @@ from typing import Any from pyvizio.const import APPS from pyvizio.util import gen_apps_list_from_url +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store @@ -22,11 +23,19 @@ _LOGGER = logging.getLogger(__name__) class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): """Define an object to hold Vizio app config data.""" - def __init__(self, hass: HomeAssistant, store: Store[list[dict[str, Any]]]) -> None: + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + store: Store[list[dict[str, Any]]], + ) -> None: """Initialize.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(days=1), ) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 9fc07dd92b0..1a53f9a5dc4 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -52,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: volvo_data = VolvoData(hass, connection, entry) - coordinator = VolvoUpdateCoordinator(hass, volvo_data) + coordinator = VolvoUpdateCoordinator(hass, entry, volvo_data) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/volvooncall/coordinator.py b/homeassistant/components/volvooncall/coordinator.py index 5ac6a58acb0..2c3e2ba365f 100644 --- a/homeassistant/components/volvooncall/coordinator.py +++ b/homeassistant/components/volvooncall/coordinator.py @@ -3,6 +3,7 @@ import asyncio import logging +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -15,12 +16,17 @@ _LOGGER = logging.getLogger(__name__) class VolvoUpdateCoordinator(DataUpdateCoordinator[None]): """Volvo coordinator.""" - def __init__(self, hass: HomeAssistant, volvo_data: VolvoData) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, volvo_data: VolvoData + ) -> None: """Initialize the data update coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="volvooncall", update_interval=DEFAULT_UPDATE_INTERVAL, ) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index b2f8ac7fd5d..fc8c6e00e84 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import CONF_STATION, DOMAIN, UPDATE_INTERVAL +from .const import DOMAIN, UPDATE_INTERVAL from .coordinator import InvalidAuth, WallboxCoordinator, async_validate_input PLATFORMS = [Platform.LOCK, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] @@ -27,11 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except InvalidAuth as ex: raise ConfigEntryAuthFailed from ex - wallbox_coordinator = WallboxCoordinator( - entry.data[CONF_STATION], - wallbox, - hass, - ) + wallbox_coordinator = WallboxCoordinator(hass, entry, wallbox) await wallbox_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 99c565d9c0c..4f20f5c406d 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -11,6 +11,7 @@ from typing import Any, Concatenate import requests from wallbox import Wallbox +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -28,6 +29,7 @@ from .const import ( CHARGER_STATUS_DESCRIPTION_KEY, CHARGER_STATUS_ID_KEY, CODE_KEY, + CONF_STATION, DOMAIN, UPDATE_INTERVAL, ChargerStatus, @@ -107,14 +109,19 @@ async def async_validate_input(hass: HomeAssistant, wallbox: Wallbox) -> None: class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Wallbox Coordinator class.""" - def __init__(self, station: str, wallbox: Wallbox, hass: HomeAssistant) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, wallbox: Wallbox + ) -> None: """Initialize.""" - self._station = station + self._station = config_entry.data[CONF_STATION] self._wallbox = wallbox super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) diff --git a/homeassistant/components/waqi/__init__.py b/homeassistant/components/waqi/__init__.py index e9feca75ee7..9821b5435d9 100644 --- a/homeassistant/components/waqi/__init__.py +++ b/homeassistant/components/waqi/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = WAQIClient(session=async_get_clientsession(hass)) client.authenticate(entry.data[CONF_API_KEY]) - waqi_coordinator = WAQIDataUpdateCoordinator(hass, client) + waqi_coordinator = WAQIDataUpdateCoordinator(hass, entry, client) await waqi_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = waqi_coordinator diff --git a/homeassistant/components/waqi/coordinator.py b/homeassistant/components/waqi/coordinator.py index d1a44e9f5b8..86f553a86cd 100644 --- a/homeassistant/components/waqi/coordinator.py +++ b/homeassistant/components/waqi/coordinator.py @@ -18,11 +18,14 @@ class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, client: WAQIClient) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, client: WAQIClient + ) -> None: """Initialize the WAQI data coordinator.""" super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(minutes=5), ) diff --git a/homeassistant/components/watergate/__init__.py b/homeassistant/components/watergate/__init__.py index fa761110339..c1747af1f11 100644 --- a/homeassistant/components/watergate/__init__.py +++ b/homeassistant/components/watergate/__init__.py @@ -16,12 +16,11 @@ from homeassistant.components.webhook import ( async_generate_url, async_register, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from .const import DOMAIN -from .coordinator import WatergateDataCoordinator +from .coordinator import WatergateConfigEntry, WatergateDataCoordinator _LOGGER = logging.getLogger(__name__) @@ -35,8 +34,6 @@ PLATFORMS: list[Platform] = [ Platform.VALVE, ] -type WatergateConfigEntry = ConfigEntry[WatergateDataCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: WatergateConfigEntry) -> bool: """Set up Watergate from a config entry.""" @@ -52,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WatergateConfigEntry) -> sonic_address if sonic_address.startswith("http") else f"http://{sonic_address}" ) - coordinator = WatergateDataCoordinator(hass, watergate_client) + coordinator = WatergateDataCoordinator(hass, entry, watergate_client) entry.runtime_data = coordinator async_register( diff --git a/homeassistant/components/watergate/coordinator.py b/homeassistant/components/watergate/coordinator.py index 1d83b7a3ccb..e3f198c144d 100644 --- a/homeassistant/components/watergate/coordinator.py +++ b/homeassistant/components/watergate/coordinator.py @@ -7,6 +7,7 @@ import logging from watergate_local_api import WatergateApiException, WatergateLocalApiClient from watergate_local_api.models import DeviceState, NetworkingData, TelemetryData +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,14 +25,25 @@ class WatergateAgregatedRequests: networking: NetworkingData +type WatergateConfigEntry = ConfigEntry[WatergateDataCoordinator] + + class WatergateDataCoordinator(DataUpdateCoordinator[WatergateAgregatedRequests]): """Class to manage fetching watergate data.""" - def __init__(self, hass: HomeAssistant, api: WatergateLocalApiClient) -> None: + config_entry: WatergateConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: WatergateConfigEntry, + api: WatergateLocalApiClient, + ) -> None: """Initialize.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(minutes=2), ) diff --git a/homeassistant/components/watergate/sensor.py b/homeassistant/components/watergate/sensor.py index 6782a93541b..44630d2f587 100644 --- a/homeassistant/components/watergate/sensor.py +++ b/homeassistant/components/watergate/sensor.py @@ -26,8 +26,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util -from . import WatergateConfigEntry -from .coordinator import WatergateAgregatedRequests, WatergateDataCoordinator +from .coordinator import ( + WatergateAgregatedRequests, + WatergateConfigEntry, + WatergateDataCoordinator, +) from .entity import WatergateEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/watergate/valve.py b/homeassistant/components/watergate/valve.py index 556b53e1d3c..ce914ebbb55 100644 --- a/homeassistant/components/watergate/valve.py +++ b/homeassistant/components/watergate/valve.py @@ -10,8 +10,7 @@ from homeassistant.components.valve import ( from homeassistant.core import callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import WatergateConfigEntry -from .coordinator import WatergateDataCoordinator +from .coordinator import WatergateConfigEntry, WatergateDataCoordinator from .entity import WatergateEntity ENTITY_NAME = "valve" diff --git a/homeassistant/components/weatherflow_cloud/__init__.py b/homeassistant/components/weatherflow_cloud/__init__.py index 8dc26f9b9c6..94c65b7c0a1 100644 --- a/homeassistant/components/weatherflow_cloud/__init__.py +++ b/homeassistant/components/weatherflow_cloud/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_TOKEN, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -15,10 +15,7 @@ PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WeatherFlowCloud from a config entry.""" - data_coordinator = WeatherFlowCloudDataUpdateCoordinator( - hass=hass, - api_token=entry.data[CONF_API_TOKEN], - ) + data_coordinator = WeatherFlowCloudDataUpdateCoordinator(hass, entry) await data_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_coordinator diff --git a/homeassistant/components/weatherflow_cloud/coordinator.py b/homeassistant/components/weatherflow_cloud/coordinator.py index 8b8a916262f..b6d2bfd5af2 100644 --- a/homeassistant/components/weatherflow_cloud/coordinator.py +++ b/homeassistant/components/weatherflow_cloud/coordinator.py @@ -6,6 +6,8 @@ from aiohttp import ClientResponseError from weatherflow4py.api import WeatherFlowRestAPI from weatherflow4py.models.rest.unified import WeatherFlowDataREST +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,12 +20,17 @@ class WeatherFlowCloudDataUpdateCoordinator( ): """Class to manage fetching REST Based WeatherFlow Forecast data.""" - def __init__(self, hass: HomeAssistant, api_token: str) -> None: + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize global WeatherFlow forecast data updater.""" - self.weather_api = WeatherFlowRestAPI(api_token=api_token) + self.weather_api = WeatherFlowRestAPI( + api_token=config_entry.data[CONF_API_TOKEN] + ) super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/weatherkit/__init__.py b/homeassistant/components/weatherkit/__init__.py index 49158182696..4cbac2b32d8 100644 --- a/homeassistant/components/weatherkit/__init__.py +++ b/homeassistant/components/weatherkit/__init__.py @@ -32,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) coordinator = WeatherKitDataUpdateCoordinator( hass=hass, + config_entry=entry, client=WeatherKitApiClient( key_id=entry.data[CONF_KEY_ID], service_id=entry.data[CONF_SERVICE_ID], diff --git a/homeassistant/components/weatherkit/coordinator.py b/homeassistant/components/weatherkit/coordinator.py index 6438d7503db..6c7119d6fb0 100644 --- a/homeassistant/components/weatherkit/coordinator.py +++ b/homeassistant/components/weatherkit/coordinator.py @@ -33,6 +33,7 @@ class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: WeatherKitApiClient, ) -> None: """Initialize.""" @@ -40,6 +41,7 @@ class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator): super().__init__( hass=hass, logger=LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(minutes=5), ) 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/weheat/__init__.py b/homeassistant/components/weheat/__init__.py index d8d8616c867..b67c3540dc5 100644 --- a/homeassistant/components/weheat/__init__.py +++ b/homeassistant/components/weheat/__init__.py @@ -8,7 +8,6 @@ import aiohttp from weheat.abstractions.discovery import HeatPumpDiscovery from weheat.exceptions import UnauthorizedException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -19,12 +18,10 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from .const import API_URL, LOGGER -from .coordinator import WeheatDataUpdateCoordinator +from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] -type WeheatConfigEntry = ConfigEntry[list[WeheatDataUpdateCoordinator]] - async def async_setup_entry(hass: HomeAssistant, entry: WeheatConfigEntry) -> bool: """Set up Weheat from a config entry.""" @@ -58,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WeheatConfigEntry) -> bo for pump_info in discovered_heat_pumps: LOGGER.debug("Adding %s", pump_info) # for each pump, add a coordinator - new_coordinator = WeheatDataUpdateCoordinator(hass, session, pump_info) + new_coordinator = WeheatDataUpdateCoordinator(hass, entry, session, pump_info) await new_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/weheat/binary_sensor.py b/homeassistant/components/weheat/binary_sensor.py index 1fb8f614a40..0ffa876ad0f 100644 --- a/homeassistant/components/weheat/binary_sensor.py +++ b/homeassistant/components/weheat/binary_sensor.py @@ -14,8 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import WeheatConfigEntry -from .coordinator import WeheatDataUpdateCoordinator +from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator from .entity import WeheatEntity # Coordinator is used to centralize the data updates diff --git a/homeassistant/components/weheat/coordinator.py b/homeassistant/components/weheat/coordinator.py index 4a85380e4a3..d7e53258e9b 100644 --- a/homeassistant/components/weheat/coordinator.py +++ b/homeassistant/components/weheat/coordinator.py @@ -13,6 +13,7 @@ from weheat.exceptions import ( UnauthorizedException, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -30,13 +31,18 @@ EXCEPTIONS = ( ApiException, ) +type WeheatConfigEntry = ConfigEntry[list[WeheatDataUpdateCoordinator]] + class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]): """A custom coordinator for the Weheat heatpump integration.""" + config_entry: WeheatConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: WeheatConfigEntry, session: OAuth2Session, heat_pump: HeatPumpDiscovery.HeatPumpInfo, ) -> None: @@ -44,6 +50,7 @@ class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]): super().__init__( hass, logger=LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) diff --git a/homeassistant/components/weheat/sensor.py b/homeassistant/components/weheat/sensor.py index 2d840aec86a..5d948c6d565 100644 --- a/homeassistant/components/weheat/sensor.py +++ b/homeassistant/components/weheat/sensor.py @@ -22,13 +22,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import WeheatConfigEntry from .const import ( DISPLAY_PRECISION_COP, DISPLAY_PRECISION_WATER_TEMP, DISPLAY_PRECISION_WATTS, ) -from .coordinator import WeheatDataUpdateCoordinator +from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator from .entity import WeheatEntity # Coordinator is used to centralize the data updates diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 83ad7bbf070..32c6a11f25c 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -78,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create the coordinator for the WS66i coordinator: Ws66iDataUpdateCoordinator = Ws66iDataUpdateCoordinator( hass, + entry, ws66i, zones, ) diff --git a/homeassistant/components/ws66i/coordinator.py b/homeassistant/components/ws66i/coordinator.py index 013e4d02b15..1b2b43963fc 100644 --- a/homeassistant/components/ws66i/coordinator.py +++ b/homeassistant/components/ws66i/coordinator.py @@ -6,6 +6,7 @@ import logging from pyws66i import WS66i, ZoneStatus +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -17,9 +18,12 @@ _LOGGER = logging.getLogger(__name__) class Ws66iDataUpdateCoordinator(DataUpdateCoordinator[list[ZoneStatus]]): """DataUpdateCoordinator to gather data for WS66i Zones.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, my_api: WS66i, zones: list[int], ) -> None: @@ -27,6 +31,7 @@ class Ws66iDataUpdateCoordinator(DataUpdateCoordinator[list[ZoneStatus]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name="WS66i", update_interval=POLL_INTERVAL, ) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index ab0d510a709..30bc7d59417 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: consoles.dict(), ) - coordinator = XboxUpdateCoordinator(hass, client, consoles) + coordinator = XboxUpdateCoordinator(hass, entry, client, consoles) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { diff --git a/homeassistant/components/xbox/coordinator.py b/homeassistant/components/xbox/coordinator.py index 4012820c43c..62c7a35e88b 100644 --- a/homeassistant/components/xbox/coordinator.py +++ b/homeassistant/components/xbox/coordinator.py @@ -20,6 +20,7 @@ from xbox.webapi.api.provider.smartglass.models import ( SmartglassConsoleStatus, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -64,9 +65,12 @@ class XboxData: class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]): """Store Xbox Console Status.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, client: XboxLiveClient, consoles: SmartglassConsoleList, ) -> None: @@ -74,6 +78,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=10), ) diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index bafc1ec543b..dd49ba502f0 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -461,7 +461,7 @@ }, "switch_set_wifi_led_on": { "name": "Switch set Wi-Fi LED on", - "description": "Turns the wifi led on.", + "description": "Turns the Wi-Fi LED on.", "fields": { "entity_id": { "name": "Entity ID", @@ -471,7 +471,7 @@ }, "switch_set_wifi_led_off": { "name": "Switch set Wi-Fi LED off", - "description": "Turn the Wi-Fi led off.", + "description": "Turns the Wi-Fi LED off.", "fields": { "entity_id": { "name": "Entity ID", @@ -567,7 +567,7 @@ }, "vacuum_goto": { "name": "Vacuum go to", - "description": "Go to the specified coordinates.", + "description": "Sends the robot to the specified coordinates.", "fields": { "x_coord": { "name": "X coordinate", diff --git a/homeassistant/components/yardian/coordinator.py b/homeassistant/components/yardian/coordinator.py index b0c8a882474..da016ca4ec4 100644 --- a/homeassistant/components/yardian/coordinator.py +++ b/homeassistant/components/yardian/coordinator.py @@ -28,6 +28,8 @@ SCAN_INTERVAL = datetime.timedelta(seconds=30) class YardianUpdateCoordinator(DataUpdateCoordinator[YardianDeviceState]): """Coordinator for Yardian API calls.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, @@ -38,6 +40,7 @@ class YardianUpdateCoordinator(DataUpdateCoordinator[YardianDeviceState]): super().__init__( hass, _LOGGER, + config_entry=entry, name=entry.title, update_interval=SCAN_INTERVAL, always_update=False, diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 004c5a70cc1..0c92aa696ca 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -152,7 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: paried_device_id := device_pairing_mapping.get(device.device_id) ) is not None: paried_device = yolink_home.get_device(paried_device_id) - device_coordinator = YoLinkCoordinator(hass, device, paried_device) + device_coordinator = YoLinkCoordinator(hass, entry, device, paried_device) try: await device_coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index b7db36541b1..d18a37bd276 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -9,6 +9,7 @@ import logging from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -21,9 +22,12 @@ _LOGGER = logging.getLogger(__name__) class YoLinkCoordinator(DataUpdateCoordinator[dict]): """YoLink DataUpdateCoordinator.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, device: YoLinkDevice, paired_device: YoLinkDevice | None = None, ) -> None: @@ -34,7 +38,11 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): data at first update """ super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=timedelta(minutes=30), ) self.device = device self.paired_device = paired_device diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 03a27b5a378..af14d597b79 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except URLError as exception: raise ConfigEntryNotReady from exception - youless_coordinator = YouLessCoordinator(hass, api) + youless_coordinator = YouLessCoordinator(hass, entry, api) await youless_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/youless/coordinator.py b/homeassistant/components/youless/coordinator.py index 0be5e463689..81e4b3a4c76 100644 --- a/homeassistant/components/youless/coordinator.py +++ b/homeassistant/components/youless/coordinator.py @@ -5,6 +5,7 @@ import logging from youless_api import YoulessAPI +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -14,10 +15,18 @@ _LOGGER = logging.getLogger(__name__) class YouLessCoordinator(DataUpdateCoordinator[None]): """Class to manage fetching YouLess data.""" - def __init__(self, hass: HomeAssistant, device: YoulessAPI) -> None: + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, device: YoulessAPI + ) -> None: """Initialize global YouLess data provider.""" super().__init__( - hass, _LOGGER, name="youless_gateway", update_interval=timedelta(seconds=10) + hass, + _LOGGER, + config_entry=config_entry, + name="youless_gateway", + update_interval=timedelta(seconds=10), ) self.device = device diff --git a/homeassistant/components/youtube/__init__.py b/homeassistant/components/youtube/__init__.py index aee4b83508c..ec8a3f325cb 100644 --- a/homeassistant/components/youtube/__init__.py +++ b/homeassistant/components/youtube/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - coordinator = YouTubeDataUpdateCoordinator(hass, auth) + coordinator = YouTubeDataUpdateCoordinator(hass, entry, auth) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index 0da480f1169..476e5bb4022 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -35,12 +35,15 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, auth: AsyncConfigEntryAuth) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, auth: AsyncConfigEntryAuth + ) -> None: """Initialize the YouTube data coordinator.""" self._auth = auth super().__init__( hass, LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(minutes=15), ) diff --git a/homeassistant/components/zamg/coordinator.py b/homeassistant/components/zamg/coordinator.py index d53c743f500..a88c97ad267 100644 --- a/homeassistant/components/zamg/coordinator.py +++ b/homeassistant/components/zamg/coordinator.py @@ -32,6 +32,7 @@ class ZamgDataUpdateCoordinator(DataUpdateCoordinator[ZamgDevice]): super().__init__( hass, LOGGER, + config_entry=entry, name=DOMAIN, update_interval=MIN_TIME_BETWEEN_UPDATES, ) diff --git a/homeassistant/components/zeversolar/coordinator.py b/homeassistant/components/zeversolar/coordinator.py index 9f6ff49eaf8..ec68cf4b56f 100644 --- a/homeassistant/components/zeversolar/coordinator.py +++ b/homeassistant/components/zeversolar/coordinator.py @@ -20,11 +20,14 @@ _LOGGER = logging.getLogger(__name__) class ZeversolarCoordinator(DataUpdateCoordinator[zeversolar.ZeverSolarData]): """Data update coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=entry, name=DOMAIN, update_interval=timedelta(minutes=1), ) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 28f029b62d5..e446f32cf08 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,6 +12,10 @@ from zha.zigbee.device import get_device_automation_triggers from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError +from homeassistant.components.homeassistant_hardware.helpers import ( + async_notify_firmware_info, + async_register_firmware_info_provider, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_TYPE, @@ -25,7 +29,7 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType -from . import repairs, websocket_api +from . import homeassistant_hardware, repairs, websocket_api from .const import ( CONF_BAUDRATE, CONF_CUSTOM_QUIRKS_PATH, @@ -110,6 +114,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ha_zha_data = HAZHAData(yaml_config=config.get(DOMAIN, {})) hass.data[DATA_ZHA] = ha_zha_data + async_register_firmware_info_provider(hass, DOMAIN, homeassistant_hardware) + return True @@ -218,6 +224,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_config) ) + if fw_info := homeassistant_hardware.get_firmware_info(hass, config_entry): + await async_notify_firmware_info( + hass, + DOMAIN, + firmware_info=fw_info, + ) + await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) 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/components/zha/homeassistant_hardware.py b/homeassistant/components/zha/homeassistant_hardware.py new file mode 100644 index 00000000000..18057d3b64d --- /dev/null +++ b/homeassistant/components/zha/homeassistant_hardware.py @@ -0,0 +1,43 @@ +"""Home Assistant Hardware firmware utilities.""" + +from __future__ import annotations + +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningIntegration, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .helpers import get_zha_gateway + + +@callback +def get_firmware_info( + hass: HomeAssistant, config_entry: ConfigEntry +) -> FirmwareInfo | None: + """Return firmware information for the ZHA instance, synchronously.""" + + # We only support EZSP firmware for now + if config_entry.data.get("radio_type", None) != "ezsp": + return None + + if (device := config_entry.data.get("device", {}).get("path")) is None: + return None + + try: + gateway = get_zha_gateway(hass) + except ValueError: + firmware_version = None + else: + firmware_version = gateway.state.node_info.version + + return FirmwareInfo( + device=device, + firmware_type=ApplicationType.EZSP, + firmware_version=firmware_version, + source=DOMAIN, + owners=[OwningIntegration(config_entry_id=config_entry.entry_id)], + ) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 6a42bc986e9..821159afb22 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.47"], + "requirements": ["zha==0.0.48"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index aaf156290a7..6a5d39bc3db 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -420,7 +420,7 @@ class ZhaMultiPANMigrationHelper: self._radio_mgr.radio_type = new_radio_type self._radio_mgr.device_path = new_device_settings[CONF_DEVICE_PATH] self._radio_mgr.device_settings = new_device_settings - device_settings = self._radio_mgr.device_settings.copy() # type: ignore[union-attr] + device_settings = self._radio_mgr.device_settings.copy() # Update the config entry settings self._hass.config_entries.async_update_entry( diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index d562a807a4f..07d897bcfd6 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -37,6 +37,7 @@ from zha.application.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ZHA_CLUSTER_HANDLER_MSG, + ZHA_GW_MSG, ) from zha.application.gateway import Gateway from zha.application.helpers import ( @@ -330,7 +331,7 @@ async def websocket_permit_devices( connection.send_message(websocket_api.event_message(msg["id"], data)) remove_dispatcher_function = async_dispatcher_connect( - hass, "zha_gateway_message", forward_messages + hass, ZHA_GW_MSG, forward_messages ) @callback diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8a5880dcde9..447b6d284f0 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -187,6 +187,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "govee_ble", "local_name": "GV5126*", }, + { + "connectable": False, + "domain": "govee_ble", + "local_name": "GV5179*", + }, { "connectable": False, "domain": "govee_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 60ac630485d..08f24577ba1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2609,7 +2609,8 @@ "name": "Home Connect", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "single_config_entry": true }, "home_plus_control": { "name": "Legrand Home+ Control", 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/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index b5f5ee9a961..3d8dc247857 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -15,7 +15,7 @@ import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout -from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver +from aiohttp_asyncmdnsresolver.api import AsyncDualMDNSResolver from homeassistant import config_entries from homeassistant.components import zeroconf @@ -377,5 +377,5 @@ def _async_get_connector( @callback -def _async_make_resolver(hass: HomeAssistant) -> AsyncMDNSResolver: - return AsyncMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) +def _async_make_resolver(hass: HomeAssistant) -> AsyncDualMDNSResolver: + return AsyncDualMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) 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/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7300b148c77..95a32696228 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -86,9 +86,7 @@ CLEANUP_INTERVAL = 3600 * 24 ORPHANED_ENTITY_KEEP_SECONDS = 3600 * 24 * 30 ENTITY_CATEGORY_VALUE_TO_INDEX: dict[EntityCategory | None, int] = { - # mypy does not understand strenum - val: idx # type: ignore[misc] - for idx, val in enumerate(EntityCategory) + val: idx for idx, val in enumerate(EntityCategory) } ENTITY_CATEGORY_INDEX_TO_VALUE = dict(enumerate(EntityCategory)) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 46bdc2b9f68..0f53b732c13 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,9 +4,9 @@ aiodhcpwatcher==1.1.0 aiodiscover==2.2.2 aiodns==3.2.0 aiohasupervisor==0.3.0 -aiohttp-asyncmdnsresolver==0.0.3 +aiohttp-asyncmdnsresolver==0.1.0 aiohttp-fast-zlib==0.2.2 -aiohttp==3.11.11 +aiohttp==3.11.12 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 @@ -15,7 +15,7 @@ async-interrupt==1.2.1 async-upnp-client==0.43.0 atomicwrites-homeassistant==1.4.1 attrs==25.1.0 -audioop-lts==0.2.1;python_version>='3.13' +audioop-lts==0.2.1 av==13.1.0 awesomeversion==24.6.0 bcrypt==4.2.0 @@ -61,9 +61,9 @@ PyTurboJPEG==1.7.5 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 -standard-aifc==3.13.0;python_version>='3.13' -standard-telnetlib==3.13.0;python_version>='3.13' +SQLAlchemy==2.0.38 +standard-aifc==3.13.0 +standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 ulid-transform==1.2.0 urllib3>=1.26.5,<2 @@ -168,6 +168,10 @@ pysnmplib==1000000000.0.0 # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 +# Poetry is a build dependency. Installing it as a runtime dependency almost +# always indicates an issue with library requirements. +poetry==1000000000.0.0 + # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 1fa93a80cd5..dc4d0988b91 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -132,7 +132,13 @@ def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) - Keep track of domains which will load but have not yet finished loading """ setup_done_futures = hass.data.setdefault(DATA_SETUP_DONE, {}) - setup_done_futures.update({domain: hass.loop.create_future() for domain in domains}) + setup_futures = hass.data.setdefault(DATA_SETUP, {}) + old_domains = set(setup_futures) | set(setup_done_futures) | hass.config.components + if overlap := old_domains & domains: + _LOGGER.debug("Domains to be loaded %s already loaded or pending", overlap) + setup_done_futures.update( + {domain: hass.loop.create_future() for domain in domains - old_domains} + ) def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: diff --git a/mypy.ini b/mypy.ini index 28c214f1eaa..2308c671197 100644 --- a/mypy.ini +++ b/mypy.ini @@ -945,6 +945,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bring.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.brother.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pyproject.toml b/pyproject.toml index 8ddf46d8be9..3936fdb3a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,16 +28,16 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.0", - "aiohttp==3.11.11", + "aiohttp==3.11.12", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.2", - "aiohttp-asyncmdnsresolver==0.0.3", + "aiohttp-asyncmdnsresolver==0.1.0", "aiozoneinfo==0.2.3", "astral==2.2", "async-interrupt==1.2.1", "attrs==25.1.0", "atomicwrites-homeassistant==1.4.1", - "audioop-lts==0.2.1;python_version>='3.13'", + "audioop-lts==0.2.1", "awesomeversion==24.6.0", "bcrypt==4.2.0", "certifi>=2021.5.30", @@ -67,9 +67,9 @@ dependencies = [ "PyYAML==6.0.2", "requests==2.32.3", "securetar==2025.1.4", - "SQLAlchemy==2.0.37", - "standard-aifc==3.13.0;python_version>='3.13'", - "standard-telnetlib==3.13.0;python_version>='3.13'", + "SQLAlchemy==2.0.38", + "standard-aifc==3.13.0", + "standard-telnetlib==3.13.0", "typing-extensions>=4.12.2,<5.0", "ulid-transform==1.2.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 diff --git a/requirements.txt b/requirements.txt index d8d7b235390..f0ff3b8054a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,16 +5,16 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.3.0 -aiohttp==3.11.11 +aiohttp==3.11.12 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.2 -aiohttp-asyncmdnsresolver==0.0.3 +aiohttp-asyncmdnsresolver==0.1.0 aiozoneinfo==0.2.3 astral==2.2 async-interrupt==1.2.1 attrs==25.1.0 atomicwrites-homeassistant==1.4.1 -audioop-lts==0.2.1;python_version>='3.13' +audioop-lts==0.2.1 awesomeversion==24.6.0 bcrypt==4.2.0 certifi>=2021.5.30 @@ -39,9 +39,9 @@ python-slugify==8.0.4 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 -standard-aifc==3.13.0;python_version>='3.13' -standard-telnetlib==3.13.0;python_version>='3.13' +SQLAlchemy==2.0.38 +standard-aifc==3.13.0 +standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 ulid-transform==1.2.0 urllib3>=1.26.5,<2 diff --git a/requirements_all.txt b/requirements_all.txt index 74d023f434a..5ebf43c5081 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.5 # homeassistant.components.vicare -PyViCare==2.41.0 +PyViCare==2.42.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 @@ -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 @@ -374,7 +374,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==12.3.2 +aioshelly==12.4.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -653,7 +653,7 @@ boto3==1.34.131 botocore==1.34.131 # homeassistant.components.bring -bring-api==1.0.0 +bring-api==1.0.2 # homeassistant.components.broadlink broadlink==0.19.0 @@ -824,10 +824,10 @@ ebusdpy==0.0.17 ecoaliface==0.4.0 # homeassistant.components.eheimdigital -eheimdigital==1.0.5 +eheimdigital==1.0.6 # homeassistant.components.electric_kiwi -electrickiwi-api==0.9.12 +electrickiwi-api==0.9.14 # homeassistant.components.elevenlabs elevenlabs==1.9.0 @@ -896,7 +896,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.21 +evohome-async==1.0.2 # homeassistant.components.bryant_evolution evolutionhttp==0.0.18 @@ -933,7 +933,7 @@ fixerio==1.0.0a0 fjaraskupan==2.3.2 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.2.1 +flexit_bacnet==2.2.3 # homeassistant.components.flipr flipr-api==1.6.1 @@ -1039,7 +1039,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==7.1.1 +google-nest-sdm==7.1.3 # homeassistant.components.google_photos google-photos-library-api==0.12.1 @@ -1055,10 +1055,10 @@ goslide-api==0.7.0 gotailwind==0.3.0 # homeassistant.components.govee_ble -govee-ble==0.42.1 +govee-ble==0.43.0 # homeassistant.components.govee_light_local -govee-local-api==2.0.0 +govee-local-api==2.0.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 @@ -1103,7 +1103,7 @@ ha-iotawattpy==0.1.2 ha-philipsjs==3.2.2 # homeassistant.components.habitica -habiticalib==0.3.4 +habiticalib==0.3.5 # homeassistant.components.bluetooth habluetooth==3.21.1 @@ -1492,7 +1492,7 @@ nextcord==2.6.0 nextdns==4.0.0 # homeassistant.components.niko_home_control -nhc==0.4.4 +nhc==0.4.10 # homeassistant.components.nibe_heatpump nibe==2.14.0 @@ -1504,7 +1504,7 @@ nice-go==1.0.1 niluclient==0.1.2 # homeassistant.components.noaa_tides -noaa-coops==0.1.9 +noaa-coops==0.4.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 @@ -1550,7 +1550,7 @@ odp-amsterdam==6.0.2 oemthermostat==1.1.1 # homeassistant.components.ohme -ohme==1.2.8 +ohme==1.2.9 # homeassistant.components.ollama ollama==0.4.7 @@ -1562,7 +1562,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 @@ -1666,7 +1666,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.6.4 +plugwise==1.7.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1903,7 +1903,7 @@ pydiscovergy==3.0.2 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2025.1.0 +pydrawise==2025.2.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 @@ -1960,7 +1960,7 @@ pyfibaro==0.8.0 pyfido==2.1.2 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.43 +pyfireservicerota==0.0.46 # homeassistant.components.flic pyflic==2.0.4 @@ -2312,6 +2312,9 @@ pysmartthings==0.7.8 # homeassistant.components.smarty pysmarty2==0.10.1 +# homeassistant.components.smhi +pysmhi==1.0.0 + # homeassistant.components.edl21 pysml==0.0.12 @@ -2443,7 +2446,7 @@ python-opensky==1.0.1 python-otbr-api==2.7.0 # homeassistant.components.overseerr -python-overseerr==0.6.0 +python-overseerr==0.7.0 # homeassistant.components.picnic python-picnic-api==1.1.0 @@ -2464,7 +2467,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 @@ -2612,7 +2615,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.11.9 +reolink-aio==0.11.10 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2744,9 +2747,6 @@ slixmpp==1.8.5 # homeassistant.components.smart_meter_texas smart-meter-texas==0.5.5 -# homeassistant.components.smhi -smhi-pkg==1.0.19 - # homeassistant.components.snapcast snapcast==2.3.6 @@ -3140,7 +3140,7 @@ zeroconf==0.143.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.47 +zha==0.0.48 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test.txt b/requirements_test.txt index 16983de5706..2731114043b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==7.6.10 freezegun==1.5.1 license-expression==30.4.1 mock-open==1.4.0 -mypy-dev==1.16.0a1 +mypy-dev==1.16.0a2 pre-commit==4.0.0 pydantic==2.10.6 pylint==3.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38d21a52091..7b6fd7f011f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.5 # homeassistant.components.vicare -PyViCare==2.41.0 +PyViCare==2.42.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 @@ -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 @@ -353,7 +353,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==12.3.2 +aioshelly==12.4.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -570,7 +570,7 @@ boschshcpy==0.2.91 botocore==1.34.131 # homeassistant.components.bring -bring-api==1.0.0 +bring-api==1.0.2 # homeassistant.components.broadlink broadlink==0.19.0 @@ -699,10 +699,10 @@ eagle100==0.1.1 easyenergy==2.1.2 # homeassistant.components.eheimdigital -eheimdigital==1.0.5 +eheimdigital==1.0.6 # homeassistant.components.electric_kiwi -electrickiwi-api==0.9.12 +electrickiwi-api==0.9.14 # homeassistant.components.elevenlabs elevenlabs==1.9.0 @@ -759,7 +759,7 @@ eternalegypt==0.0.16 eufylife-ble-client==0.1.8 # homeassistant.components.evohome -evohome-async==0.4.21 +evohome-async==1.0.2 # homeassistant.components.bryant_evolution evolutionhttp==0.0.18 @@ -789,7 +789,7 @@ fivem-api==0.1.2 fjaraskupan==2.3.2 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.2.1 +flexit_bacnet==2.2.3 # homeassistant.components.flipr flipr-api==1.6.1 @@ -886,7 +886,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==7.1.1 +google-nest-sdm==7.1.3 # homeassistant.components.google_photos google-photos-library-api==0.12.1 @@ -902,10 +902,10 @@ goslide-api==0.7.0 gotailwind==0.3.0 # homeassistant.components.govee_ble -govee-ble==0.42.1 +govee-ble==0.43.0 # homeassistant.components.govee_light_local -govee-local-api==2.0.0 +govee-local-api==2.0.1 # homeassistant.components.gpsd gps3==0.33.3 @@ -941,7 +941,7 @@ ha-iotawattpy==0.1.2 ha-philipsjs==3.2.2 # homeassistant.components.habitica -habiticalib==0.3.4 +habiticalib==0.3.5 # homeassistant.components.bluetooth habluetooth==3.21.1 @@ -1252,7 +1252,7 @@ nextcord==2.6.0 nextdns==4.0.0 # homeassistant.components.niko_home_control -nhc==0.4.4 +nhc==0.4.10 # homeassistant.components.nibe_heatpump nibe==2.14.0 @@ -1295,7 +1295,7 @@ objgraph==3.5.0 odp-amsterdam==6.0.2 # homeassistant.components.ohme -ohme==1.2.8 +ohme==1.2.9 # homeassistant.components.ollama ollama==0.4.7 @@ -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 @@ -1376,7 +1376,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.6.4 +plugwise==1.7.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1550,7 +1550,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.2 # homeassistant.components.hydrawise -pydrawise==2025.1.0 +pydrawise==2025.2.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 @@ -1595,7 +1595,7 @@ pyfibaro==0.8.0 pyfido==2.1.2 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.43 +pyfireservicerota==0.0.46 # homeassistant.components.flic pyflic==2.0.4 @@ -1881,6 +1881,9 @@ pysmartthings==0.7.8 # homeassistant.components.smarty pysmarty2==0.10.1 +# homeassistant.components.smhi +pysmhi==1.0.0 + # homeassistant.components.edl21 pysml==0.0.12 @@ -1976,7 +1979,7 @@ python-opensky==1.0.1 python-otbr-api==2.7.0 # homeassistant.components.overseerr -python-overseerr==0.6.0 +python-overseerr==0.7.0 # homeassistant.components.picnic python-picnic-api==1.1.0 @@ -1994,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 @@ -2109,7 +2112,7 @@ renault-api==0.2.9 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.11.9 +reolink-aio==0.11.10 # homeassistant.components.rflink rflink==0.0.66 @@ -2205,9 +2208,6 @@ slack_sdk==3.33.4 # homeassistant.components.smart_meter_texas smart-meter-texas==0.5.5 -# homeassistant.components.smhi -smhi-pkg==1.0.19 - # homeassistant.components.snapcast snapcast==2.3.6 @@ -2523,7 +2523,7 @@ zeroconf==0.143.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.47 +zha==0.0.48 # homeassistant.components.zwave_js zwave-js-server-python==0.60.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ef57b9140ce..fa823fa4834 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -199,6 +199,10 @@ pysnmplib==1000000000.0.0 # breaks getmac due to them both sharing the same python package name inside 'getmac'. get-mac==1000000000.0.0 +# Poetry is a build dependency. Installing it as a runtime dependency almost +# always indicates an issue with library requirements. +poetry==1000000000.0.0 + # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. 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/assist_satellite/test_intent.py b/tests/components/assist_satellite/test_intent.py index 27107c7d2e9..9304229dbe3 100644 --- a/tests/components/assist_satellite/test_intent.py +++ b/tests/components/assist_satellite/test_intent.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from .conftest import MockAssistSatellite +from .conftest import TEST_DOMAIN, MockAssistSatellite @pytest.fixture @@ -65,12 +65,7 @@ async def test_broadcast_intent( }, "language": "en", "response_type": "action_done", - "speech": { - "plain": { - "extra_data": None, - "speech": "Done", - } - }, + "speech": {}, # response comes from intents } assert len(entity.announcements) == 1 assert len(entity2.announcements) == 1 @@ -99,12 +94,37 @@ async def test_broadcast_intent( }, "language": "en", "response_type": "action_done", - "speech": { - "plain": { - "extra_data": None, - "speech": "Done", - } - }, + "speech": {}, # response comes from intents } assert len(entity.announcements) == 1 assert len(entity2.announcements) == 2 + + +async def test_broadcast_intent_excluded_domains( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + entity2: MockAssistSatellite, + mock_tts: None, +) -> None: + """Test that the broadcast intent filters out entities in excluded domains.""" + + # Exclude the "test" domain + with patch( + "homeassistant.components.assist_satellite.intent.EXCLUDED_DOMAINS", + new={TEST_DOMAIN}, + ): + result = await intent.async_handle( + hass, "test", intent.INTENT_BROADCAST, {"message": {"value": "Hello"}} + ) + assert result.as_dict() == { + "card": {}, + "data": { + "failed": [], + "success": [], # no satellites + "targets": [], + }, + "language": "en", + "response_type": "action_done", + "speech": {}, + } diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index 2b2e9257097..da630f7fbc8 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -4,11 +4,13 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch import uuid -from bring_api.types import ( +from bring_api import ( + BringActivityResponse, BringAuthResponse, BringItemsResponse, BringListResponse, BringUserSettingsResponse, + BringUsersResponse, ) import pytest @@ -60,6 +62,13 @@ def mock_bring_client() -> Generator[AsyncMock]: client.get_all_user_settings.return_value = BringUserSettingsResponse.from_json( load_fixture("usersettings.json", DOMAIN) ) + client.get_activity.return_value = BringActivityResponse.from_json( + load_fixture("activity.json", DOMAIN) + ) + client.get_list_users.return_value = BringUsersResponse.from_json( + load_fixture("users.json", DOMAIN) + ) + yield client diff --git a/tests/components/bring/fixtures/activity.json b/tests/components/bring/fixtures/activity.json new file mode 100644 index 00000000000..5e9a8c089d3 --- /dev/null +++ b/tests/components/bring/fixtures/activity.json @@ -0,0 +1,62 @@ +{ + "timeline": [ + { + "type": "LIST_ITEMS_CHANGED", + "content": { + "uuid": "673594a9-f92d-4cb6-adf1-d2f7a83207a4", + "purchase": [ + { + "uuid": "658a3770-1a03-4ee0-94a6-10362a642377", + "itemId": "Gurke", + "specification": "", + "attributes": [] + } + ], + "recently": [ + { + "uuid": "1ed22d3d-f19b-4530-a518-19872da3fd3e", + "itemId": "Milch", + "specification": "", + "attributes": [] + } + ], + "sessionDate": "2025-01-01T03:09:33.036Z", + "publicUserUuid": "9a21fdfc-63a4-441a-afc1-ef3030605a9d" + } + }, + { + "type": "LIST_ITEMS_ADDED", + "content": { + "uuid": "9a16635c-dea2-4e00-904a-c5034f9cfecf", + "items": [ + { + "uuid": "66a633a2-ae09-47bf-8845-3c0198480544", + "itemId": "Joghurt", + "specification": "", + "attributes": [] + } + ], + "sessionDate": "2025-01-01T02:54:57.656Z", + "publicUserUuid": "73af455f-c158-4004-a5e0-79f4f8a6d4bd" + } + }, + { + "type": "LIST_ITEMS_REMOVED", + "content": { + "uuid": "303dedf6-d4b2-4d25-a8cd-1c7967b84fcb", + "items": [ + { + "uuid": "2ba8ddb6-01c6-4b0b-a89d-f3da6b291528", + "itemId": "Tofu", + "specification": "", + "attributes": [] + } + ], + "sessionDate": "2025-01-01T03:09:12.380Z", + "publicUserUuid": "7d5e9d08-877a-4c36-8740-a9bf74ec690a" + } + } + ], + "timestamp": "2025-01-01T03:09:33.036Z", + "totalEvents": 3 +} diff --git a/tests/components/bring/fixtures/users.json b/tests/components/bring/fixtures/users.json new file mode 100644 index 00000000000..c9393dcb20d --- /dev/null +++ b/tests/components/bring/fixtures/users.json @@ -0,0 +1,31 @@ +{ + "users": [ + { + "publicUuid": "9a21fdfc-63a4-441a-afc1-ef3030605a9d", + "name": "Bring", + "email": "test-email", + "photoPath": "", + "pushEnabled": true, + "plusTryOut": false, + "country": "DE", + "language": "de" + }, + { + "publicUuid": "73af455f-c158-4004-a5e0-79f4f8a6d4bd", + "name": "NAME", + "email": "EMAIL", + "photoPath": "", + "pushEnabled": true, + "plusTryOut": false, + "country": "US", + "language": "en" + }, + { + "publicUuid": "7d5e9d08-877a-4c36-8740-a9bf74ec690a", + "pushEnabled": true, + "plusTryOut": false, + "country": "US", + "language": "en" + } + ] +} diff --git a/tests/components/bring/snapshots/test_diagnostics.ambr b/tests/components/bring/snapshots/test_diagnostics.ambr index 740f4902fc3..951c3d3f808 100644 --- a/tests/components/bring/snapshots/test_diagnostics.ambr +++ b/tests/components/bring/snapshots/test_diagnostics.ambr @@ -1,113 +1,407 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'b4776778-7f6c-496e-951b-92a35d3db0dd': dict({ - 'content': dict({ - 'items': dict({ - 'purchase': list([ + 'data': dict({ + 'b4776778-7f6c-496e-951b-92a35d3db0dd': dict({ + 'activity': dict({ + 'timeline': list([ dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, + 'content': dict({ + 'items': list([ + ]), + 'publicUserUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'purchase': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Gurke', + 'specification': '', + 'uuid': '658a3770-1a03-4ee0-94a6-10362a642377', }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Paprika', - 'specification': 'Rot', - 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Milch', + 'specification': '', + 'uuid': '1ed22d3d-f19b-4530-a518-19872da3fd3e', + }), + ]), + 'sessionDate': '2025-01-01T03:09:33.036000+00:00', + 'uuid': '673594a9-f92d-4cb6-adf1-d2f7a83207a4', + }), + 'type': 'LIST_ITEMS_CHANGED', }), dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, + 'content': dict({ + 'items': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Joghurt', + 'specification': '', + 'uuid': '66a633a2-ae09-47bf-8845-3c0198480544', }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Pouletbrüstli', - 'specification': 'Bio', - 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', + ]), + 'publicUserUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', + 'purchase': list([ + ]), + 'recently': list([ + ]), + 'sessionDate': '2025-01-01T02:54:57.656000+00:00', + 'uuid': '9a16635c-dea2-4e00-904a-c5034f9cfecf', + }), + 'type': 'LIST_ITEMS_ADDED', + }), + dict({ + 'content': dict({ + 'items': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Tofu', + 'specification': '', + 'uuid': '2ba8ddb6-01c6-4b0b-a89d-f3da6b291528', + }), + ]), + 'publicUserUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', + 'purchase': list([ + ]), + 'recently': list([ + ]), + 'sessionDate': '2025-01-01T03:09:12.380000+00:00', + 'uuid': '303dedf6-d4b2-4d25-a8cd-1c7967b84fcb', + }), + 'type': 'LIST_ITEMS_REMOVED', }), ]), - 'recently': list([ + 'timestamp': '2025-01-01T03:09:33.036000+00:00', + 'totalEvents': 3, + }), + 'content': dict({ + 'items': dict({ + 'purchase': list([ + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Paprika', + 'specification': 'Rot', + 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', + }), + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Pouletbrüstli', + 'specification': 'Bio', + 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Ananas', + 'specification': '', + 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', + }), + ]), + }), + 'status': 'REGISTERED', + 'uuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + }), + 'lst': dict({ + 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + 'name': 'Baumarkt', + 'theme': 'ch.publisheria.bring.theme.home', + }), + 'users': dict({ + 'users': list([ dict({ - 'attributes': list([ - ]), - 'itemId': 'Ananas', - 'specification': '', - 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', + 'country': 'DE', + 'email': 'test-email', + 'language': 'de', + 'name': 'Bring', + 'photoPath': '', + 'plusTryOut': False, + 'publicUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': 'EMAIL', + 'language': 'en', + 'name': 'NAME', + 'photoPath': '', + 'plusTryOut': False, + 'publicUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': None, + 'language': 'en', + 'name': None, + 'photoPath': None, + 'plusTryOut': False, + 'publicUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', + 'pushEnabled': True, }), ]), }), - 'status': 'REGISTERED', - 'uuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', }), - 'lst': dict({ - 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', - 'name': 'Baumarkt', - 'theme': 'ch.publisheria.bring.theme.home', + 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5': dict({ + 'activity': dict({ + 'timeline': list([ + dict({ + 'content': dict({ + 'items': list([ + ]), + 'publicUserUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'purchase': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Gurke', + 'specification': '', + 'uuid': '658a3770-1a03-4ee0-94a6-10362a642377', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Milch', + 'specification': '', + 'uuid': '1ed22d3d-f19b-4530-a518-19872da3fd3e', + }), + ]), + 'sessionDate': '2025-01-01T03:09:33.036000+00:00', + 'uuid': '673594a9-f92d-4cb6-adf1-d2f7a83207a4', + }), + 'type': 'LIST_ITEMS_CHANGED', + }), + dict({ + 'content': dict({ + 'items': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Joghurt', + 'specification': '', + 'uuid': '66a633a2-ae09-47bf-8845-3c0198480544', + }), + ]), + 'publicUserUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', + 'purchase': list([ + ]), + 'recently': list([ + ]), + 'sessionDate': '2025-01-01T02:54:57.656000+00:00', + 'uuid': '9a16635c-dea2-4e00-904a-c5034f9cfecf', + }), + 'type': 'LIST_ITEMS_ADDED', + }), + dict({ + 'content': dict({ + 'items': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Tofu', + 'specification': '', + 'uuid': '2ba8ddb6-01c6-4b0b-a89d-f3da6b291528', + }), + ]), + 'publicUserUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', + 'purchase': list([ + ]), + 'recently': list([ + ]), + 'sessionDate': '2025-01-01T03:09:12.380000+00:00', + 'uuid': '303dedf6-d4b2-4d25-a8cd-1c7967b84fcb', + }), + 'type': 'LIST_ITEMS_REMOVED', + }), + ]), + 'timestamp': '2025-01-01T03:09:33.036000+00:00', + 'totalEvents': 3, + }), + 'content': dict({ + 'items': dict({ + 'purchase': list([ + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Paprika', + 'specification': 'Rot', + 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', + }), + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Pouletbrüstli', + 'specification': 'Bio', + 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Ananas', + 'specification': '', + 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', + }), + ]), + }), + 'status': 'REGISTERED', + 'uuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', + }), + 'lst': dict({ + 'listUuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', + 'name': 'Einkauf', + 'theme': 'ch.publisheria.bring.theme.home', + }), + 'users': dict({ + 'users': list([ + dict({ + 'country': 'DE', + 'email': 'test-email', + 'language': 'de', + 'name': 'Bring', + 'photoPath': '', + 'plusTryOut': False, + 'publicUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': 'EMAIL', + 'language': 'en', + 'name': 'NAME', + 'photoPath': '', + 'plusTryOut': False, + 'publicUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': None, + 'language': 'en', + 'name': None, + 'photoPath': None, + 'plusTryOut': False, + 'publicUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', + 'pushEnabled': True, + }), + ]), + }), }), }), - 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5': dict({ - 'content': dict({ - 'items': dict({ - 'purchase': list([ - dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, - }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Paprika', - 'specification': 'Rot', - 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', - }), - dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, - }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Pouletbrüstli', - 'specification': 'Bio', - 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', - }), - ]), - 'recently': list([ - dict({ - 'attributes': list([ - ]), - 'itemId': 'Ananas', - 'specification': '', - 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', - }), - ]), - }), - 'status': 'REGISTERED', - 'uuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', - }), - 'lst': dict({ + 'lists': list([ + dict({ 'listUuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', 'name': 'Einkauf', 'theme': 'ch.publisheria.bring.theme.home', }), + dict({ + 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + 'name': 'Baumarkt', + 'theme': 'ch.publisheria.bring.theme.home', + }), + ]), + 'user_settings': dict({ + 'userlistsettings': list([ + dict({ + 'listUuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', + 'usersettings': list([ + dict({ + 'key': 'listSectionOrder', + 'value': '["Früchte & Gemüse","Brot & Gebäck","Milch & Käse","Fleisch & Fisch","Zutaten & Gewürze","Fertig- & Tiefkühlprodukte","Getreideprodukte","Snacks & Süsswaren","Getränke & Tabak","Haushalt & Gesundheit","Pflege & Gesundheit","Tierbedarf","Baumarkt & Garten","Eigene Artikel"]', + }), + dict({ + 'key': 'listArticleLanguage', + 'value': 'de-DE', + }), + ]), + }), + dict({ + 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + 'usersettings': list([ + dict({ + 'key': 'listSectionOrder', + 'value': '["Früchte & Gemüse","Brot & Gebäck","Milch & Käse","Fleisch & Fisch","Zutaten & Gewürze","Fertig- & Tiefkühlprodukte","Getreideprodukte","Snacks & Süsswaren","Getränke & Tabak","Haushalt & Gesundheit","Pflege & Gesundheit","Tierbedarf","Baumarkt & Garten","Eigene Artikel"]', + }), + dict({ + 'key': 'listArticleLanguage', + 'value': 'en-US', + }), + ]), + }), + ]), + 'usersettings': list([ + dict({ + 'key': 'autoPush', + 'value': 'ON', + }), + dict({ + 'key': 'premiumHideOffersBadge', + 'value': 'ON', + }), + dict({ + 'key': 'premiumHideSponsoredCategories', + 'value': 'ON', + }), + dict({ + 'key': 'premiumHideInspirationsBadge', + 'value': 'ON', + }), + dict({ + 'key': 'onboardClient', + 'value': 'android', + }), + dict({ + 'key': 'premiumHideOffersOnMain', + 'value': 'ON', + }), + dict({ + 'key': 'defaultListUUID', + 'value': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', + }), + ]), }), }) # --- diff --git a/tests/components/bring/snapshots/test_event.ambr b/tests/components/bring/snapshots/test_event.ambr new file mode 100644 index 00000000000..907467bd6bb --- /dev/null +++ b/tests/components/bring/snapshots/test_event.ambr @@ -0,0 +1,167 @@ +# serializer version: 1 +# name: test_setup[event.baumarkt_activities-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'list_items_changed', + 'list_items_added', + 'list_items_removed', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.baumarkt_activities', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activities', + 'platform': 'bring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activities', + 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_activities', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.baumarkt_activities-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://api.getbring.com/rest/v2/bringusers/profilepictures/9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'event_type': 'list_items_changed', + 'event_types': list([ + 'list_items_changed', + 'list_items_added', + 'list_items_removed', + ]), + 'friendly_name': 'Baumarkt Activities', + 'items': list([ + ]), + 'last_activity_by': 'Bring', + 'publicUserUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'purchase': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Gurke', + 'specification': '', + 'uuid': '658a3770-1a03-4ee0-94a6-10362a642377', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Milch', + 'specification': '', + 'uuid': '1ed22d3d-f19b-4530-a518-19872da3fd3e', + }), + ]), + 'sessionDate': HAFakeDatetime(2025, 1, 1, 3, 9, 33, 36000, tzinfo=datetime.timezone.utc), + 'uuid': '673594a9-f92d-4cb6-adf1-d2f7a83207a4', + }), + 'context': , + 'entity_id': 'event.baumarkt_activities', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-01-01T03:30:00.000+00:00', + }) +# --- +# name: test_setup[event.einkauf_activities-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'list_items_changed', + 'list_items_added', + 'list_items_removed', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.einkauf_activities', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activities', + 'platform': 'bring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activities', + 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_activities', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.einkauf_activities-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://api.getbring.com/rest/v2/bringusers/profilepictures/9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'event_type': 'list_items_changed', + 'event_types': list([ + 'list_items_changed', + 'list_items_added', + 'list_items_removed', + ]), + 'friendly_name': 'Einkauf Activities', + 'items': list([ + ]), + 'last_activity_by': 'Bring', + 'publicUserUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'purchase': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Gurke', + 'specification': '', + 'uuid': '658a3770-1a03-4ee0-94a6-10362a642377', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Milch', + 'specification': '', + 'uuid': '1ed22d3d-f19b-4530-a518-19872da3fd3e', + }), + ]), + 'sessionDate': HAFakeDatetime(2025, 1, 1, 3, 9, 33, 36000, tzinfo=datetime.timezone.utc), + 'uuid': '673594a9-f92d-4cb6-adf1-d2f7a83207a4', + }), + 'context': , + 'entity_id': 'event.einkauf_activities', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-01-01T03:30:00.000+00:00', + }) +# --- diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 93e86051a75..b9208324c61 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -2,11 +2,7 @@ from unittest.mock import AsyncMock -from bring_api.exceptions import ( - BringAuthException, - BringParseException, - BringRequestException, -) +from bring_api import BringAuthException, BringParseException, BringRequestException import pytest from homeassistant.components.bring.const import DOMAIN @@ -214,3 +210,104 @@ async def test_flow_reauth_unique_id_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unique_id_mismatch" + + +@pytest.mark.usefixtures("mock_bring_client") +async def test_flow_reconfigure( + hass: HomeAssistant, bring_config_entry: MockConfigEntry +) -> None: + """Test reconfigure flow.""" + bring_config_entry.add_to_hass(hass) + result = await bring_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert bring_config_entry.data[CONF_EMAIL] == "new-email" + assert bring_config_entry.data[CONF_PASSWORD] == "new-password" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (BringRequestException(), "cannot_connect"), + (BringAuthException(), "invalid_auth"), + (BringParseException(), "unknown"), + (IndexError(), "unknown"), + ], +) +async def test_flow_reconfigure_errors( + hass: HomeAssistant, + mock_bring_client: AsyncMock, + bring_config_entry: MockConfigEntry, + raise_error: Exception, + text_error: str, +) -> None: + """Test reconfigure flow errors.""" + bring_config_entry.add_to_hass(hass) + result = await bring_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_bring_client.login.side_effect = raise_error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": text_error} + + mock_bring_client.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert bring_config_entry.data[CONF_EMAIL] == "new-email" + assert bring_config_entry.data[CONF_PASSWORD] == "new-password" + + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_flow_reconfigure_unique_id_mismatch( + hass: HomeAssistant, + bring_config_entry: MockConfigEntry, + mock_bring_client: AsyncMock, +) -> None: + """Test we abort reconfigure if unique id mismatch.""" + + mock_bring_client.uuid = "11111111-11111111-11111111-11111111" + + bring_config_entry.add_to_hass(hass) + + result = await bring_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" diff --git a/tests/components/bring/test_event.py b/tests/components/bring/test_event.py new file mode 100644 index 00000000000..99b96c27153 --- /dev/null +++ b/tests/components/bring/test_event.py @@ -0,0 +1,46 @@ +"""Test for event platform of the Bring! integration.""" + +from collections.abc import Generator +from unittest.mock import patch + +from freezegun.api import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +def event_only() -> Generator[None]: + """Enable only the event platform.""" + with patch( + "homeassistant.components.bring.PLATFORMS", + [Platform.EVENT], + ): + yield + + +@pytest.mark.usefixtures("mock_bring_client") +@freeze_time("2025-01-01T03:30:00.000Z") +async def test_setup( + hass: HomeAssistant, + bring_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test states of event platform.""" + + bring_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(bring_config_entry.entry_id) + await hass.async_block_till_done() + + assert bring_config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform( + hass, entity_registry, snapshot, bring_config_entry.entry_id + ) diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index a77c709315f..f053f294ef1 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -142,7 +142,6 @@ async def test_config_entry_not_ready_udpdate_failed( @pytest.mark.parametrize( ("exception", "state"), [ - (None, ConfigEntryState.LOADED), (BringAuthException, ConfigEntryState.SETUP_ERROR), (BringRequestException, ConfigEntryState.SETUP_RETRY), (BringParseException, ConfigEntryState.SETUP_RETRY), @@ -159,9 +158,8 @@ async def test_config_entry_not_ready_auth_error( mock_bring_client.load_lists.side_effect = [ mock_bring_client.load_lists.return_value, - BringAuthException, + exception, ] - mock_bring_client.retrieve_new_access_token.side_effect = exception bring_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(bring_config_entry.entry_id) diff --git a/tests/components/bring/test_util.py b/tests/components/bring/test_util.py index 3060f31c134..673c4e68a4d 100644 --- a/tests/components/bring/test_util.py +++ b/tests/components/bring/test_util.py @@ -1,6 +1,12 @@ """Test for utility functions of the Bring! integration.""" -from bring_api import BringItemsResponse, BringListResponse, BringUserSettingsResponse +from bring_api import ( + BringActivityResponse, + BringItemsResponse, + BringListResponse, + BringUserSettingsResponse, +) +from bring_api.types import BringUsersResponse import pytest from homeassistant.components.bring.const import DOMAIN @@ -41,9 +47,10 @@ def test_sum_attributes(attribute: str, expected: int) -> None: """Test function sum_attributes.""" items = BringItemsResponse.from_json(load_fixture("items.json", DOMAIN)) lst = BringListResponse.from_json(load_fixture("lists.json", DOMAIN)) - + activity = BringActivityResponse.from_json(load_fixture("activity.json", DOMAIN)) + users = BringUsersResponse.from_json(load_fixture("users.json", DOMAIN)) result = sum_attributes( - BringData(lst.lists[0], items), + BringData(lst.lists[0], items, activity, users), attribute, ) diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 7002f7c39ec..276a06a7f46 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -9,6 +9,7 @@ from hass_nabucasa import Cloud from hass_nabucasa.auth import CognitoAuth from hass_nabucasa.cloudhooks import Cloudhooks from hass_nabucasa.const import DEFAULT_SERVERS, DEFAULT_VALUES, STATE_CONNECTED +from hass_nabucasa.files import Files from hass_nabucasa.google_report_state import GoogleReportState from hass_nabucasa.ice_servers import IceServers from hass_nabucasa.iot import CloudIoT @@ -68,6 +69,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]: spec=CloudIoT, last_disconnect_reason=None, state=STATE_CONNECTED ) mock_cloud.voice = MagicMock(spec=Voice) + mock_cloud.files = MagicMock(spec=Files) mock_cloud.started = None mock_cloud.ice_servers = MagicMock( spec=IceServers, diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index 5b2b8751311..6e59b7d983e 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -1,14 +1,15 @@ """Test the cloud backup platform.""" -from collections.abc import AsyncGenerator, AsyncIterator, Generator +from collections.abc import AsyncGenerator, Generator from io import StringIO from typing import Any from unittest.mock import Mock, PropertyMock, patch from aiohttp import ClientError from hass_nabucasa import CloudError +from hass_nabucasa.api import CloudApiNonRetryableError +from hass_nabucasa.files import FilesError import pytest -from yarl import URL from homeassistant.components.backup import ( DOMAIN as BACKUP_DOMAIN, @@ -22,11 +23,20 @@ from homeassistant.components.cloud.const import EVENT_CLOUD_EVENT from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component +from homeassistant.util.aiohttp import MockStreamReader from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator, MagicMock, WebSocketGenerator +class MockStreamReaderChunked(MockStreamReader): + """Mock a stream reader with simulated chunked data.""" + + async def readchunk(self) -> tuple[bytes, bool]: + """Read bytes.""" + return (self._content.read(), False) + + @pytest.fixture(autouse=True) async def setup_integration( hass: HomeAssistant, @@ -55,49 +65,6 @@ def mock_delete_file() -> Generator[MagicMock]: yield delete_file -@pytest.fixture -def mock_get_download_details() -> Generator[MagicMock]: - """Mock list files.""" - with patch( - "homeassistant.components.cloud.backup.async_files_download_details", - spec_set=True, - ) as download_details: - download_details.return_value = { - "url": ( - "https://blabla.cloudflarestorage.com/blabla/backup/" - "462e16810d6841228828d9dd2f9e341e.tar?X-Amz-Algorithm=blah" - ), - } - yield download_details - - -@pytest.fixture -def mock_get_upload_details() -> Generator[MagicMock]: - """Mock list files.""" - with patch( - "homeassistant.components.cloud.backup.async_files_upload_details", - spec_set=True, - ) as download_details: - download_details.return_value = { - "url": ( - "https://blabla.cloudflarestorage.com/blabla/backup/" - "ea5c969e492c49df89d432a1483b8dc3.tar?X-Amz-Algorithm=blah" - ), - "headers": { - "content-md5": "HOhSM3WZkpHRYGiz4YRGIQ==", - "x-amz-meta-storage-type": "backup", - "x-amz-meta-b64json": ( - "eyJhZGRvbnMiOltdLCJiYWNrdXBfaWQiOiJjNDNiNWU2MCIsImRhdGUiOiIyMDI0LT" - "EyLTAzVDA0OjI1OjUwLjMyMDcwMy0wNTowMCIsImRhdGFiYXNlX2luY2x1ZGVkIjpm" - "YWxzZSwiZm9sZGVycyI6W10sImhvbWVhc3Npc3RhbnRfaW5jbHVkZWQiOnRydWUsIm" - "hvbWVhc3Npc3RhbnRfdmVyc2lvbiI6IjIwMjQuMTIuMC5kZXYwIiwibmFtZSI6ImVy" - "aWsiLCJwcm90ZWN0ZWQiOnRydWUsInNpemUiOjM1NjI0OTYwfQ==" - ), - }, - } - yield download_details - - @pytest.fixture def mock_list_files() -> Generator[MagicMock]: """Mock list files.""" @@ -264,52 +231,30 @@ async def test_agents_download( hass: HomeAssistant, hass_client: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - mock_get_download_details: Mock, + cloud: Mock, ) -> None: """Test agent download backup.""" client = await hass_client() backup_id = "23e64aec" - aioclient_mock.get( - mock_get_download_details.return_value["url"], content=b"backup data" - ) + cloud.files.download.return_value = MockStreamReaderChunked(b"backup data") resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud") assert resp.status == 200 assert await resp.content.read() == b"backup data" -@pytest.mark.parametrize("side_effect", [ClientError, CloudError]) -@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") -async def test_agents_download_fail_cloud( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_get_download_details: Mock, - side_effect: Exception, -) -> None: - """Test agent download backup, when cloud user is logged in.""" - client = await hass_client() - backup_id = "23e64aec" - mock_get_download_details.side_effect = side_effect - - resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud") - assert resp.status == 500 - content = await resp.content.read() - assert "Failed to get download details" in content.decode() - - @pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") async def test_agents_download_fail_get( hass: HomeAssistant, hass_client: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - mock_get_download_details: Mock, + cloud: Mock, ) -> None: """Test agent download backup, when cloud user is logged in.""" client = await hass_client() backup_id = "23e64aec" - aioclient_mock.get(mock_get_download_details.return_value["url"], status=500) + cloud.files.download.side_effect = FilesError("Oh no :(") resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud") assert resp.status == 500 @@ -336,8 +281,7 @@ async def test_agents_upload( hass: HomeAssistant, hass_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, - aioclient_mock: AiohttpClientMocker, - mock_get_upload_details: Mock, + cloud: Mock, ) -> None: """Test agent upload backup.""" client = await hass_client() @@ -355,8 +299,6 @@ async def test_agents_upload( protected=True, size=0, ) - aioclient_mock.put(mock_get_upload_details.return_value["url"]) - with ( patch( "homeassistant.components.backup.manager.BackupManager.async_get_backup", @@ -374,26 +316,22 @@ async def test_agents_upload( data={"file": StringIO("test")}, ) - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][0] == "PUT" - assert aioclient_mock.mock_calls[-1][1] == URL( - mock_get_upload_details.return_value["url"] - ) - assert isinstance(aioclient_mock.mock_calls[-1][2], AsyncIterator) + assert len(cloud.files.upload.mock_calls) == 1 + metadata = cloud.files.upload.mock_calls[-1].kwargs["metadata"] + assert metadata["backup_id"] == backup_id assert resp.status == 201 assert f"Uploading backup {backup_id}" in caplog.text -@pytest.mark.parametrize("put_mock_kwargs", [{"status": 500}, {"exc": TimeoutError}]) +@pytest.mark.parametrize("side_effect", [FilesError("Boom!"), CloudError("Boom!")]) @pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") -async def test_agents_upload_fail_put( +async def test_agents_upload_fail( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_storage: dict[str, Any], - aioclient_mock: AiohttpClientMocker, - mock_get_upload_details: Mock, - put_mock_kwargs: dict[str, Any], + side_effect: Exception, + cloud: Mock, caplog: pytest.LogCaptureFixture, ) -> None: """Test agent upload backup fails.""" @@ -412,7 +350,8 @@ async def test_agents_upload_fail_put( protected=True, size=0, ) - aioclient_mock.put(mock_get_upload_details.return_value["url"], **put_mock_kwargs) + + cloud.files.upload.side_effect = side_effect with ( patch( @@ -435,9 +374,9 @@ async def test_agents_upload_fail_put( ) await hass.async_block_till_done() - assert len(aioclient_mock.mock_calls) == 2 assert "Failed to upload backup, retrying (2/2) in 60s" in caplog.text assert resp.status == 201 + assert cloud.files.upload.call_count == 2 store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"] assert len(store_backups) == 1 stored_backup = store_backups[0] @@ -445,19 +384,32 @@ async def test_agents_upload_fail_put( assert stored_backup["failed_agent_ids"] == ["cloud.cloud"] -@pytest.mark.parametrize("side_effect", [ClientError, CloudError]) -@pytest.mark.usefixtures("cloud_logged_in") -async def test_agents_upload_fail_cloud( +@pytest.mark.parametrize( + ("side_effect", "logmsg"), + [ + ( + CloudApiNonRetryableError("Boom!", code="NC-SH-FH-03"), + "The backup size of 13.37GB is too large to be uploaded to Home Assistant Cloud", + ), + ( + CloudApiNonRetryableError("Boom!", code="NC-CE-01"), + "Failed to upload backup Boom!", + ), + ], +) +@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files") +async def test_agents_upload_fail_non_retryable( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_storage: dict[str, Any], - mock_get_upload_details: Mock, side_effect: Exception, + logmsg: str, + cloud: Mock, + caplog: pytest.LogCaptureFixture, ) -> None: - """Test agent upload backup, when cloud user is logged in.""" + """Test agent upload backup fails with non-retryable error.""" client = await hass_client() backup_id = "test-backup" - mock_get_upload_details.side_effect = side_effect test_backup = AgentBackup( addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], backup_id=backup_id, @@ -469,8 +421,11 @@ async def test_agents_upload_fail_cloud( homeassistant_version="2024.12.0", name="Test", protected=True, - size=0, + size=14358124749, ) + + cloud.files.upload.side_effect = side_effect + with ( patch( "homeassistant.components.backup.manager.BackupManager.async_get_backup", @@ -480,7 +435,6 @@ async def test_agents_upload_fail_cloud( return_value=test_backup, ), patch("pathlib.Path.open") as mocked_open, - patch("homeassistant.components.cloud.backup.asyncio.sleep"), ): mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) fetch_backup.return_value = test_backup @@ -490,7 +444,9 @@ async def test_agents_upload_fail_cloud( ) await hass.async_block_till_done() + assert logmsg in caplog.text assert resp.status == 201 + assert cloud.files.upload.call_count == 1 store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"] assert len(store_backups) == 1 stored_backup = store_backups[0] 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/evohome/conftest.py b/tests/components/evohome/conftest.py index 6daab3f32bb..5f60bc418e3 100644 --- a/tests/components/evohome/conftest.py +++ b/tests/components/evohome/conftest.py @@ -3,26 +3,26 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Callable -from datetime import datetime, timedelta, timezone +from datetime import timedelta, timezone from http import HTTPMethod from typing import Any from unittest.mock import MagicMock, patch -from aiohttp import ClientSession from evohomeasync2 import EvohomeClient -from evohomeasync2.broker import Broker -from evohomeasync2.controlsystem import ControlSystem +from evohomeasync2.auth import AbstractTokenManager, Auth +from evohomeasync2.control_system import ControlSystem from evohomeasync2.zone import Zone +from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.evohome import CONF_PASSWORD, CONF_USERNAME, DOMAIN -from homeassistant.const import Platform +from homeassistant.components.evohome.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify from homeassistant.util.json import JsonArrayType, JsonObjectType -from .const import ACCESS_TOKEN, REFRESH_TOKEN, USERNAME +from .const import ACCESS_TOKEN, REFRESH_TOKEN, SESSION_ID, USERNAME from tests.common import load_json_array_fixture, load_json_object_fixture @@ -64,44 +64,69 @@ def zone_schedule_fixture(install: str) -> JsonObjectType: return load_json_object_fixture("default/schedule_zone.json", DOMAIN) -def mock_get_factory(install: str) -> Callable: +def mock_post_request(install: str) -> Callable: + """Obtain an access token via a POST to the vendor's web API.""" + + async def post_request( + self: AbstractTokenManager, url: str, /, **kwargs: Any + ) -> JsonArrayType | JsonObjectType: + """Obtain an access token via a POST to the vendor's web API.""" + + if "Token" in url: + return { + "access_token": f"new_{ACCESS_TOKEN}", + "token_type": "bearer", + "expires_in": 1800, + "refresh_token": f"new_{REFRESH_TOKEN}", + # "scope": "EMEA-V1-Basic EMEA-V1-Anonymous", # optional + } + + if "session" in url: + return {"sessionId": f"new_{SESSION_ID}"} + + pytest.fail(f"Unexpected request: {HTTPMethod.POST} {url}") + + return post_request + + +def mock_make_request(install: str) -> Callable: """Return a get method for a specified installation.""" - async def mock_get( - self: Broker, url: str, **kwargs: Any + async def make_request( + self: Auth, method: HTTPMethod, url: str, **kwargs: Any ) -> JsonArrayType | JsonObjectType: """Return the JSON for a HTTP get of a given URL.""" - # a proxy for the behaviour of the real web API - if self.refresh_token is None: - self.refresh_token = f"new_{REFRESH_TOKEN}" + if method != HTTPMethod.GET: + pytest.fail(f"Unmocked method: {method} {url}") - if ( - self.access_token_expires is None - or self.access_token_expires < datetime.now() - ): - self.access_token = f"new_{ACCESS_TOKEN}" - self.access_token_expires = datetime.now() + timedelta(minutes=30) + await self._headers() # assume a valid GET, and return the JSON for that web API - if url == "userAccount": # userAccount + if url == "accountInfo": # /v0/accountInfo + return {} # will throw a KeyError -> BadApiResponseError + + if url.startswith("locations/"): # /v0/locations?userId={id}&allData=True + return [] # user has no locations + + if url == "userAccount": # /v2/userAccount return user_account_config_fixture(install) - if url.startswith("location"): - if "installationInfo" in url: # location/installationInfo?userId={id} + if url.startswith("location/"): + if "installationInfo" in url: # /v2/location/installationInfo?userId={id} return user_locations_config_fixture(install) - if "location" in url: # location/{id}/status + if "status" in url: # /v2/location/{id}/status return location_status_fixture(install) elif "schedule" in url: - if url.startswith("domesticHotWater"): # domesticHotWater/{id}/schedule + if url.startswith("domesticHotWater"): # /v2/domesticHotWater/{id}/schedule return dhw_schedule_fixture(install) - if url.startswith("temperatureZone"): # temperatureZone/{id}/schedule + if url.startswith("temperatureZone"): # /v2/temperatureZone/{id}/schedule return zone_schedule_fixture(install) pytest.fail(f"Unexpected request: {HTTPMethod.GET} {url}") - return mock_get + return make_request @pytest.fixture @@ -137,9 +162,13 @@ async def setup_evohome( dt_util.set_default_time_zone(timezone(timedelta(minutes=utc_offset))) with ( - patch("homeassistant.components.evohome.evo.EvohomeClient") as mock_client, - patch("homeassistant.components.evohome.ev1.EvohomeClient", return_value=None), - patch("evohomeasync2.broker.Broker.get", mock_get_factory(install)), + # patch("homeassistant.components.evohome.ec1.EvohomeClient", return_value=None), + patch("homeassistant.components.evohome.ec2.EvohomeClient") as mock_client, + patch( + "evohomeasync2.auth.CredentialsManagerBase._post_request", + mock_post_request(install), + ), + patch("evohome.auth.AbstractAuth._make_request", mock_make_request(install)), ): evo: EvohomeClient | None = None @@ -155,12 +184,11 @@ async def setup_evohome( mock_client.assert_called_once() - assert mock_client.call_args.args[0] == config[CONF_USERNAME] - assert mock_client.call_args.args[1] == config[CONF_PASSWORD] + assert isinstance(evo, EvohomeClient) + assert evo._token_manager.client_id == config[CONF_USERNAME] + assert evo._token_manager._secret == config[CONF_PASSWORD] - assert isinstance(mock_client.call_args.kwargs["session"], ClientSession) - - assert evo and evo.account_info is not None + assert evo.user_account mock_client.return_value = evo yield mock_client @@ -170,39 +198,32 @@ async def setup_evohome( async def evohome( hass: HomeAssistant, config: dict[str, str], + freezer: FrozenDateTimeFactory, install: str, ) -> AsyncGenerator[MagicMock]: """Return the mocked evohome client for this install fixture.""" + freezer.move_to("2024-07-10T12:00:00Z") # so schedules are as expected + async for mock_client in setup_evohome(hass, config, install=install): yield mock_client @pytest.fixture -async def ctl_id( - hass: HomeAssistant, - config: dict[str, str], - install: MagicMock, -) -> AsyncGenerator[str]: +def ctl_id(evohome: MagicMock) -> str: """Return the entity_id of the evohome integration's controller.""" - async for mock_client in setup_evohome(hass, config, install=install): - evo: EvohomeClient = mock_client.return_value - ctl: ControlSystem = evo._get_single_tcs() + evo: EvohomeClient = evohome.return_value + ctl: ControlSystem = evo.tcs - yield f"{Platform.CLIMATE}.{slugify(ctl.location.name)}" + return f"{Platform.CLIMATE}.{slugify(ctl.location.name)}" @pytest.fixture -async def zone_id( - hass: HomeAssistant, - config: dict[str, str], - install: MagicMock, -) -> AsyncGenerator[str]: +def zone_id(evohome: MagicMock) -> str: """Return the entity_id of the evohome integration's first zone.""" - async for mock_client in setup_evohome(hass, config, install=install): - evo: EvohomeClient = mock_client.return_value - zone: Zone = list(evo._get_single_tcs().zones.values())[0] + evo: EvohomeClient = evohome.return_value + zone: Zone = evo.tcs.zones[0] - yield f"{Platform.CLIMATE}.{slugify(zone.name)}" + return f"{Platform.CLIMATE}.{slugify(zone.name)}" diff --git a/tests/components/evohome/fixtures/h032585/user_locations.json b/tests/components/evohome/fixtures/h032585/user_locations.json index b4ea2e5c420..c291d591c99 100644 --- a/tests/components/evohome/fixtures/h032585/user_locations.json +++ b/tests/components/evohome/fixtures/h032585/user_locations.json @@ -3,6 +3,7 @@ "locationInfo": { "locationId": "111111", "name": "My Home", + "useDaylightSaveSwitching": true, "timeZone": { "timeZoneId": "GMTStandardTime", "displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London", diff --git a/tests/components/evohome/fixtures/h099625/user_locations.json b/tests/components/evohome/fixtures/h099625/user_locations.json index cc32caccc73..31cac00ae9e 100644 --- a/tests/components/evohome/fixtures/h099625/user_locations.json +++ b/tests/components/evohome/fixtures/h099625/user_locations.json @@ -3,6 +3,7 @@ "locationInfo": { "locationId": "111111", "name": "My Home", + "useDaylightSaveSwitching": true, "timeZone": { "timeZoneId": "FLEStandardTime", "displayName": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius", diff --git a/tests/components/evohome/snapshots/test_climate.ambr b/tests/components/evohome/snapshots/test_climate.ambr index ce7fcf2744e..23a15e3f64f 100644 --- a/tests/components/evohome/snapshots/test_climate.ambr +++ b/tests/components/evohome/snapshots/test_climate.ambr @@ -2,120 +2,120 @@ # name: test_ctl_set_hvac_mode[default] list([ tuple( - 'HeatingOff', + , ), tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_set_hvac_mode[h032585] list([ tuple( - 'Off', + , ), tuple( - 'Heat', + , ), ]) # --- # name: test_ctl_set_hvac_mode[h099625] list([ tuple( - 'HeatingOff', + , ), tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_set_hvac_mode[minimal] list([ tuple( - 'HeatingOff', + , ), tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_set_hvac_mode[sys_004] list([ tuple( - 'HeatingOff', + , ), tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_turn_off[default] list([ tuple( - 'HeatingOff', + , ), ]) # --- # name: test_ctl_turn_off[h032585] list([ tuple( - 'Off', + , ), ]) # --- # name: test_ctl_turn_off[h099625] list([ tuple( - 'HeatingOff', + , ), ]) # --- # name: test_ctl_turn_off[minimal] list([ tuple( - 'HeatingOff', + , ), ]) # --- # name: test_ctl_turn_off[sys_004] list([ tuple( - 'HeatingOff', + , ), ]) # --- # name: test_ctl_turn_on[default] list([ tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_turn_on[h032585] list([ tuple( - 'Heat', + , ), ]) # --- # name: test_ctl_turn_on[h099625] list([ tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_turn_on[minimal] list([ tuple( - 'Auto', + , ), ]) # --- # name: test_ctl_turn_on[sys_004] list([ tuple( - 'Auto', + , ), ]) # --- @@ -137,16 +137,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 16.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -184,16 +184,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -230,21 +230,21 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ + 'activeFaults': tuple( dict({ - 'faultType': 'TempZoneActuatorLowBattery', - 'since': '2022-03-02T04:50:20', + 'fault_type': 'TempZoneActuatorLowBattery', + 'since': '2022-03-02T04:50:20+00:00', }), - ]), + ), 'setpoint_status': dict({ 'setpoint_mode': 'TemporaryOverride', 'target_heat_temperature': 21.0, - 'until': '2022-03-07T20:00:00+01:00', + 'until': '2022-03-07T19:00:00+00:00', }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -282,16 +282,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -329,16 +329,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -376,16 +376,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 16.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -423,20 +423,20 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ + 'activeFaults': tuple( dict({ - 'faultType': 'TempZoneActuatorCommunicationLost', - 'since': '2022-03-02T15:56:01', + 'fault_type': 'TempZoneActuatorCommunicationLost', + 'since': '2022-03-02T15:56:01+00:00', }), - ]), + ), 'setpoint_status': dict({ 'setpoint_mode': 'PermanentOverride', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -477,8 +477,8 @@ 'Custom', ]), 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + ), 'system_id': '3432522', 'system_mode_status': dict({ 'is_permanent': True, @@ -513,16 +513,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 16.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -560,16 +560,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -606,17 +606,17 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'TemporaryOverride', 'target_heat_temperature': 21.0, - 'until': '2022-03-07T20:00:00+01:00', + 'until': '2022-03-07T19:00:00+00:00', }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -654,16 +654,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -701,16 +701,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -748,16 +748,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 16.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -795,16 +795,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'PermanentOverride', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -845,8 +845,8 @@ 'Custom', ]), 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + ), 'system_id': '3432522', 'system_mode_status': dict({ 'is_permanent': True, @@ -881,16 +881,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'PermanentOverride', 'target_heat_temperature': 14.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -923,8 +923,8 @@ 'max_temp': 35, 'min_temp': 7, 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + ), 'system_id': '416856', 'system_mode_status': dict({ 'is_permanent': True, @@ -959,16 +959,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 21.5, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -1006,8 +1006,8 @@ 'away', ]), 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + ), 'system_id': '8557535', 'system_mode_status': dict({ 'is_permanent': True, @@ -1042,16 +1042,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 21.5, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+03:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/Kiev')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+03:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Kiev')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -1089,16 +1089,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 21.5, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+03:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/Kiev')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+03:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Kiev')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -1136,16 +1136,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'FollowSchedule', 'target_heat_temperature': 17.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -1186,8 +1186,8 @@ 'Custom', ]), 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + ), 'system_id': '3432522', 'system_mode_status': dict({ 'is_permanent': True, @@ -1222,8 +1222,12 @@ 'away', ]), 'status': dict({ - 'active_system_faults': list([ - ]), + 'activeSystemFaults': tuple( + dict({ + 'fault_type': 'GatewayCommunicationLost', + 'since': '2023-05-04T18:47:36.772704+02:00', + }), + ), 'system_id': '4187769', 'system_mode_status': dict({ 'is_permanent': True, @@ -1258,16 +1262,16 @@ 'permanent', ]), 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'setpoint_status': dict({ 'setpoint_mode': 'PermanentOverride', 'target_heat_temperature': 15.0, }), 'setpoints': dict({ - 'next_sp_from': '2024-07-10T22:10:00+02:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')), 'next_sp_temp': 18.6, - 'this_sp_from': '2024-07-10T08:00:00+02:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')), 'this_sp_temp': 16.0, }), 'temperature_status': dict({ @@ -1331,7 +1335,7 @@ 17.0, ), dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- @@ -1344,7 +1348,7 @@ 21.5, ), dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- @@ -1357,7 +1361,7 @@ 21.5, ), dict({ - 'until': datetime.datetime(2024, 7, 10, 19, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 19, 10, tzinfo=datetime.timezone.utc), }), ]) # --- @@ -1370,7 +1374,7 @@ 17.0, ), dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- @@ -1383,35 +1387,35 @@ 15.0, ), dict({ - 'until': datetime.datetime(2024, 7, 10, 20, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 20, 10, tzinfo=datetime.timezone.utc), }), ]) # --- # name: test_zone_set_temperature[default] list([ dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- # name: test_zone_set_temperature[h032585] list([ dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- # name: test_zone_set_temperature[h099625] list([ dict({ - 'until': datetime.datetime(2024, 7, 10, 19, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 19, 10, tzinfo=datetime.timezone.utc), }), ]) # --- # name: test_zone_set_temperature[minimal] list([ dict({ - 'until': datetime.datetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 21, 10, tzinfo=datetime.timezone.utc), }), ]) # --- diff --git a/tests/components/evohome/snapshots/test_water_heater.ambr b/tests/components/evohome/snapshots/test_water_heater.ambr index 4cdeb28f445..771e2c20cba 100644 --- a/tests/components/evohome/snapshots/test_water_heater.ambr +++ b/tests/components/evohome/snapshots/test_water_heater.ambr @@ -2,10 +2,10 @@ # name: test_set_operation_mode[default] list([ dict({ - 'until': datetime.datetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc), }), dict({ - 'until': datetime.datetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc), + 'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc), }), ]) # --- @@ -13,11 +13,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'away_mode': 'on', - 'current_temperature': 23, + 'current_temperature': 23.0, 'friendly_name': 'Domestic Hot Water', 'icon': 'mdi:thermometer-lines', - 'max_temp': 60, - 'min_temp': 43, + 'max_temp': 60.0, + 'min_temp': 43.3, 'operation_list': list([ 'auto', 'on', @@ -25,13 +25,13 @@ ]), 'operation_mode': 'off', 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'dhw_id': '3933910', 'setpoints': dict({ - 'next_sp_from': '2024-07-10T13:00:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 13, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_state': 'Off', - 'this_sp_from': '2024-07-10T12:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_state': 'On', }), 'state_status': dict({ @@ -60,11 +60,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'away_mode': 'on', - 'current_temperature': 23, + 'current_temperature': 23.0, 'friendly_name': 'Domestic Hot Water', 'icon': 'mdi:thermometer-lines', - 'max_temp': 60, - 'min_temp': 43, + 'max_temp': 60.0, + 'min_temp': 43.3, 'operation_list': list([ 'auto', 'on', @@ -72,13 +72,13 @@ ]), 'operation_mode': 'off', 'status': dict({ - 'active_faults': list([ - ]), + 'activeFaults': tuple( + ), 'dhw_id': '3933910', 'setpoints': dict({ - 'next_sp_from': '2024-07-10T13:00:00+01:00', + 'next_sp_from': HAFakeDatetime(2024, 7, 10, 13, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'next_sp_state': 'Off', - 'this_sp_from': '2024-07-10T12:00:00+01:00', + 'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')), 'this_sp_state': 'On', }), 'state_status': dict({ diff --git a/tests/components/evohome/test_climate.py b/tests/components/evohome/test_climate.py index 325dd914bc0..b1b930c6382 100644 --- a/tests/components/evohome/test_climate.py +++ b/tests/components/evohome/test_climate.py @@ -65,7 +65,7 @@ async def test_ctl_set_hvac_mode( results = [] # SERVICE_SET_HVAC_MODE: HVACMode.OFF - with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn: + with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_SET_HVAC_MODE, @@ -76,14 +76,15 @@ async def test_ctl_set_hvac_mode( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args != () # 'HeatingOff' or 'Off' - assert mock_fcn.await_args.kwargs == {"until": None} + try: + mock_fcn.assert_awaited_once_with("HeatingOff", until=None) + except AssertionError: + mock_fcn.assert_awaited_once_with("Off", until=None) - results.append(mock_fcn.await_args.args) + results.append(mock_fcn.await_args.args) # type: ignore[union-attr] # SERVICE_SET_HVAC_MODE: HVACMode.HEAT - with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn: + with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_SET_HVAC_MODE, @@ -94,11 +95,12 @@ async def test_ctl_set_hvac_mode( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args != () # 'Auto' or 'Heat' - assert mock_fcn.await_args.kwargs == {"until": None} + try: + mock_fcn.assert_awaited_once_with("Auto", until=None) + except AssertionError: + mock_fcn.assert_awaited_once_with("Heat", until=None) - results.append(mock_fcn.await_args.args) + results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @@ -134,7 +136,7 @@ async def test_ctl_turn_off( results = [] # SERVICE_TURN_OFF - with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn: + with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_TURN_OFF, @@ -144,11 +146,12 @@ async def test_ctl_turn_off( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args != () # 'HeatingOff' or 'Off' - assert mock_fcn.await_args.kwargs == {"until": None} + try: + mock_fcn.assert_awaited_once_with("HeatingOff", until=None) + except AssertionError: + mock_fcn.assert_awaited_once_with("Off", until=None) - results.append(mock_fcn.await_args.args) + results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @@ -164,7 +167,7 @@ async def test_ctl_turn_on( results = [] # SERVICE_TURN_ON - with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn: + with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_TURN_ON, @@ -174,11 +177,12 @@ async def test_ctl_turn_on( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args != () # 'Auto' or 'Heat' - assert mock_fcn.await_args.kwargs == {"until": None} + try: + mock_fcn.assert_awaited_once_with("Auto", until=None) + except AssertionError: + mock_fcn.assert_awaited_once_with("Heat", until=None) - results.append(mock_fcn.await_args.args) + results.append(mock_fcn.await_args.args) # type: ignore[union-attr] assert results == snapshot @@ -194,7 +198,7 @@ async def test_zone_set_hvac_mode( results = [] # SERVICE_SET_HVAC_MODE: HVACMode.HEAT - with patch("evohomeasync2.zone.Zone.reset_mode") as mock_fcn: + with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_SET_HVAC_MODE, @@ -205,9 +209,7 @@ async def test_zone_set_hvac_mode( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() # SERVICE_SET_HVAC_MODE: HVACMode.OFF with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: @@ -221,7 +223,9 @@ async def test_zone_set_hvac_mode( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # minimum target temp assert mock_fcn.await_args.kwargs == {"until": None} @@ -243,7 +247,7 @@ async def test_zone_set_preset_mode( results = [] # SERVICE_SET_PRESET_MODE: none - with patch("evohomeasync2.zone.Zone.reset_mode") as mock_fcn: + with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_SET_PRESET_MODE, @@ -254,9 +258,7 @@ async def test_zone_set_preset_mode( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() # SERVICE_SET_PRESET_MODE: permanent with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn: @@ -270,7 +272,9 @@ async def test_zone_set_preset_mode( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # current target temp assert mock_fcn.await_args.kwargs == {"until": None} @@ -288,7 +292,9 @@ async def test_zone_set_preset_mode( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # current target temp assert mock_fcn.await_args.kwargs != {} # next setpoint dtm @@ -302,12 +308,10 @@ async def test_zone_set_preset_mode( async def test_zone_set_temperature( hass: HomeAssistant, zone_id: str, - freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test SERVICE_SET_TEMPERATURE of an evohome heating zone.""" - freezer.move_to("2024-07-10T12:00:00Z") results = [] # SERVICE_SET_TEMPERATURE: temperature @@ -322,7 +326,9 @@ async def test_zone_set_temperature( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args == (19.1,) assert mock_fcn.await_args.kwargs != {} # next setpoint dtm @@ -352,7 +358,9 @@ async def test_zone_turn_off( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args != () # minimum target temp assert mock_fcn.await_args.kwargs == {"until": None} @@ -369,7 +377,7 @@ async def test_zone_turn_on( """Test SERVICE_TURN_ON of an evohome heating zone.""" # SERVICE_TURN_ON - with patch("evohomeasync2.zone.Zone.reset_mode") as mock_fcn: + with patch("evohomeasync2.zone.Zone.reset") as mock_fcn: await hass.services.async_call( Platform.CLIMATE, SERVICE_TURN_ON, @@ -379,6 +387,4 @@ async def test_zone_turn_on( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() diff --git a/tests/components/evohome/test_coordinator.py b/tests/components/evohome/test_coordinator.py new file mode 100644 index 00000000000..7fb325d55b9 --- /dev/null +++ b/tests/components/evohome/test_coordinator.py @@ -0,0 +1,55 @@ +"""The tests for the evohome coordinator.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from evohomeasync2 import EvohomeClient +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.evohome import EvoData +from homeassistant.components.evohome.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import UpdateFailed + +from tests.common import async_fire_time_changed + + +@pytest.mark.parametrize("install", ["minimal"]) +async def test_setup_platform( + hass: HomeAssistant, + config: dict[str, str], + evohome: EvohomeClient, + freezer: FrozenDateTimeFactory, +) -> None: + """Test entities and their states after setup of evohome.""" + + evo_data: EvoData = hass.data.get(DOMAIN) # type: ignore[assignment] + update_interval: timedelta = evo_data.coordinator.update_interval # type: ignore[assignment] + + # confirm initial state after coordinator.async_first_refresh()... + state = hass.states.get("climate.my_home") + assert state is not None and state.state != STATE_UNAVAILABLE + + with patch( + "homeassistant.components.evohome.coordinator.EvoDataUpdateCoordinator._async_update_data", + side_effect=UpdateFailed, + ): + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # confirm appropriate response to loss of state... + state = hass.states.get("climate.my_home") + assert state is not None and state.state == STATE_UNAVAILABLE + + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # if coordinator is working, the state will be restored + state = hass.states.get("climate.my_home") + assert state is not None and state.state != STATE_UNAVAILABLE diff --git a/tests/components/evohome/test_init.py b/tests/components/evohome/test_init.py index 9b5fe6ad62d..d327bdf14b4 100644 --- a/tests/components/evohome/test_init.py +++ b/tests/components/evohome/test_init.py @@ -4,80 +4,130 @@ from __future__ import annotations from http import HTTPStatus import logging -from unittest.mock import patch +from unittest.mock import Mock, patch +import aiohttp from evohomeasync2 import EvohomeClient, exceptions as exc -from evohomeasync2.broker import _ERR_MSG_LOOKUP_AUTH, _ERR_MSG_LOOKUP_BASE import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion -from homeassistant.components.evohome import DOMAIN, EvoService +from homeassistant.components.evohome.const import DOMAIN, EvoService from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .conftest import mock_post_request from .const import TEST_INSTALLS -SETUP_FAILED_ANTICIPATED = ( +_MSG_429 = ( + "You have exceeded the server's API rate limit. Wait a while " + "and try again (consider reducing your polling interval)." +) +_MSG_OTH = ( + "Unable to contact the vendor's server. Check your network " + "and review the vendor's status page, https://status.resideo.com." +) +_MSG_USR = ( + "Failed to authenticate. Check the username/password. Note that some " + "special characters accepted via the vendor's website are not valid here." +) + +LOG_HINT_429_CREDS = ("evohome.credentials", logging.ERROR, _MSG_429) +LOG_HINT_OTH_CREDS = ("evohome.credentials", logging.ERROR, _MSG_OTH) +LOG_HINT_USR_CREDS = ("evohome.credentials", logging.ERROR, _MSG_USR) + +LOG_HINT_429_AUTH = ("evohome.auth", logging.ERROR, _MSG_429) +LOG_HINT_OTH_AUTH = ("evohome.auth", logging.ERROR, _MSG_OTH) +LOG_HINT_USR_AUTH = ("evohome.auth", logging.ERROR, _MSG_USR) + +LOG_FAIL_CONNECTION = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: Authenticator response is invalid: Connection error", +) +LOG_FAIL_CREDENTIALS = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "Authenticator response is invalid: {'error': 'invalid_grant'}", +) +LOG_FAIL_GATEWAY = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "Authenticator response is invalid: 502 Bad Gateway, response=None", +) +LOG_FAIL_TOO_MANY = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "Authenticator response is invalid: 429 Too Many Requests, response=None", +) + +LOG_FGET_CONNECTION = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "GET https://tccna.resideo.com/WebAPI/emea/api/v1/userAccount: " + "Connection error", +) +LOG_FGET_GATEWAY = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "GET https://tccna.resideo.com/WebAPI/emea/api/v1/userAccount: " + "502 Bad Gateway, response=None", +) +LOG_FGET_TOO_MANY = ( + "homeassistant.components.evohome", + logging.ERROR, + "Failed to fetch initial data: " + "GET https://tccna.resideo.com/WebAPI/emea/api/v1/userAccount: " + "429 Too Many Requests, response=None", +) + + +LOG_SETUP_FAILED = ( "homeassistant.setup", logging.ERROR, "Setup failed for 'evohome': Integration failed to initialize.", ) -SETUP_FAILED_UNEXPECTED = ( - "homeassistant.setup", - logging.ERROR, - "Error during setup of component evohome: ", + +EXC_BAD_CONNECTION = aiohttp.ClientConnectionError( + "Connection error", ) -AUTHENTICATION_FAILED = ( - "homeassistant.components.evohome.helpers", - logging.ERROR, - "Failed to authenticate with the vendor's server. Check your username" - " and password. NB: Some special password characters that work" - " correctly via the website will not work via the web API. Message" - " is: ", +EXC_BAD_CREDENTIALS = exc.AuthenticationFailedError( + "Authenticator response is invalid: {'error': 'invalid_grant'}", + status=HTTPStatus.BAD_REQUEST, ) -REQUEST_FAILED_NONE = ( - "homeassistant.components.evohome.helpers", - logging.WARNING, - "Unable to connect with the vendor's server. " - "Check your network and the vendor's service status page. " - "Message is: ", +EXC_TOO_MANY_REQUESTS = aiohttp.ClientResponseError( + Mock(), + (), + status=HTTPStatus.TOO_MANY_REQUESTS, + message=HTTPStatus.TOO_MANY_REQUESTS.phrase, ) -REQUEST_FAILED_503 = ( - "homeassistant.components.evohome.helpers", - logging.WARNING, - "The vendor says their server is currently unavailable. " - "Check the vendor's service status page", -) -REQUEST_FAILED_429 = ( - "homeassistant.components.evohome.helpers", - logging.WARNING, - "The vendor's API rate limit has been exceeded. " - "If this message persists, consider increasing the scan_interval", +EXC_BAD_GATEWAY = aiohttp.ClientResponseError( + Mock(), (), status=HTTPStatus.BAD_GATEWAY, message=HTTPStatus.BAD_GATEWAY.phrase ) -REQUEST_FAILED_LOOKUP = { - None: [ - REQUEST_FAILED_NONE, - SETUP_FAILED_ANTICIPATED, - ], - HTTPStatus.SERVICE_UNAVAILABLE: [ - REQUEST_FAILED_503, - SETUP_FAILED_ANTICIPATED, - ], - HTTPStatus.TOO_MANY_REQUESTS: [ - REQUEST_FAILED_429, - SETUP_FAILED_ANTICIPATED, - ], +AUTHENTICATION_TESTS: dict[Exception, list] = { + EXC_BAD_CONNECTION: [LOG_HINT_OTH_CREDS, LOG_FAIL_CONNECTION, LOG_SETUP_FAILED], + EXC_BAD_CREDENTIALS: [LOG_HINT_USR_CREDS, LOG_FAIL_CREDENTIALS, LOG_SETUP_FAILED], + EXC_BAD_GATEWAY: [LOG_HINT_OTH_CREDS, LOG_FAIL_GATEWAY, LOG_SETUP_FAILED], + EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_CREDS, LOG_FAIL_TOO_MANY, LOG_SETUP_FAILED], +} + +CLIENT_REQUEST_TESTS: dict[Exception, list] = { + EXC_BAD_CONNECTION: [LOG_HINT_OTH_AUTH, LOG_FGET_CONNECTION, LOG_SETUP_FAILED], + EXC_BAD_GATEWAY: [LOG_HINT_OTH_AUTH, LOG_FGET_GATEWAY, LOG_SETUP_FAILED], + EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_AUTH, LOG_FGET_TOO_MANY, LOG_SETUP_FAILED], } -@pytest.mark.parametrize( - "status", [*sorted([*_ERR_MSG_LOOKUP_AUTH, HTTPStatus.BAD_GATEWAY]), None] -) +@pytest.mark.parametrize("exception", AUTHENTICATION_TESTS) async def test_authentication_failure_v2( hass: HomeAssistant, config: dict[str, str], - status: HTTPStatus, + exception: Exception, caplog: pytest.LogCaptureFixture, ) -> None: """Test failure to setup an evohome-compatible system. @@ -85,27 +135,24 @@ async def test_authentication_failure_v2( In this instance, the failure occurs in the v2 API. """ - with patch("evohomeasync2.broker.Broker.get") as mock_fcn: - mock_fcn.side_effect = exc.AuthenticationFailed("", status=status) - - with caplog.at_level(logging.WARNING): - result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + with ( + patch( + "evohome.credentials.CredentialsManagerBase._request", side_effect=exception + ), + caplog.at_level(logging.WARNING), + ): + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) assert result is False - assert caplog.record_tuples == [ - AUTHENTICATION_FAILED, - SETUP_FAILED_ANTICIPATED, - ] + assert caplog.record_tuples == AUTHENTICATION_TESTS[exception] -@pytest.mark.parametrize( - "status", [*sorted([*_ERR_MSG_LOOKUP_BASE, HTTPStatus.BAD_GATEWAY]), None] -) +@pytest.mark.parametrize("exception", CLIENT_REQUEST_TESTS) async def test_client_request_failure_v2( hass: HomeAssistant, config: dict[str, str], - status: HTTPStatus, + exception: Exception, caplog: pytest.LogCaptureFixture, ) -> None: """Test failure to setup an evohome-compatible system. @@ -113,17 +160,19 @@ async def test_client_request_failure_v2( In this instance, the failure occurs in the v2 API. """ - with patch("evohomeasync2.broker.Broker.get") as mock_fcn: - mock_fcn.side_effect = exc.RequestFailed("", status=status) - - with caplog.at_level(logging.WARNING): - result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + with ( + patch( + "evohomeasync2.auth.CredentialsManagerBase._post_request", + mock_post_request("default"), + ), + patch("evohome.auth.AbstractAuth._request", side_effect=exception), + caplog.at_level(logging.WARNING), + ): + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) assert result is False - assert caplog.record_tuples == REQUEST_FAILED_LOOKUP.get( - status, [SETUP_FAILED_UNEXPECTED] - ) + assert caplog.record_tuples == CLIENT_REQUEST_TESTS[exception] @pytest.mark.parametrize("install", [*TEST_INSTALLS, "botched"]) @@ -148,7 +197,7 @@ async def test_service_refresh_system( """Test EvoService.REFRESH_SYSTEM of an evohome system.""" # EvoService.REFRESH_SYSTEM - with patch("evohomeasync2.location.Location.refresh_status") as mock_fcn: + with patch("evohomeasync2.location.Location.update") as mock_fcn: await hass.services.async_call( DOMAIN, EvoService.REFRESH_SYSTEM, @@ -156,9 +205,7 @@ async def test_service_refresh_system( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() @pytest.mark.parametrize("install", ["default"]) @@ -169,7 +216,7 @@ async def test_service_reset_system( """Test EvoService.RESET_SYSTEM of an evohome system.""" # EvoService.RESET_SYSTEM (if SZ_AUTO_WITH_RESET in modes) - with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn: + with patch("evohomeasync2.control_system.ControlSystem.set_mode") as mock_fcn: await hass.services.async_call( DOMAIN, EvoService.RESET_SYSTEM, @@ -177,6 +224,4 @@ async def test_service_reset_system( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == ("AutoWithReset",) - assert mock_fcn.await_args.kwargs == {"until": None} + mock_fcn.assert_awaited_once_with("AutoWithReset", until=None) diff --git a/tests/components/evohome/test_storage.py b/tests/components/evohome/test_storage.py index b3597352487..4528f1c8590 100644 --- a/tests/components/evohome/test_storage.py +++ b/tests/components/evohome/test_storage.py @@ -7,13 +7,7 @@ from typing import Any, Final, NotRequired, TypedDict import pytest -from homeassistant.components.evohome import ( - CONF_USERNAME, - DOMAIN, - STORAGE_KEY, - STORAGE_VER, - dt_aware_to_naive, -) +from homeassistant.components.evohome.const import DOMAIN, STORAGE_KEY, STORAGE_VER from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -22,7 +16,8 @@ from .const import ACCESS_TOKEN, REFRESH_TOKEN, SESSION_ID, USERNAME class _SessionDataT(TypedDict): - sessionId: str + session_id: str + session_id_expires: NotRequired[str] # 2024-07-27T23:57:30+01:00 class _TokenStoreT(TypedDict): @@ -65,7 +60,7 @@ _TEST_STORAGE_BASE: Final[_TokenStoreT] = { TEST_STORAGE_DATA: Final[dict[str, _TokenStoreT]] = { "sans_session_id": _TEST_STORAGE_BASE, "null_session_id": _TEST_STORAGE_BASE | {SZ_USER_DATA: None}, # type: ignore[dict-item] - "with_session_id": _TEST_STORAGE_BASE | {SZ_USER_DATA: {"sessionId": SESSION_ID}}, + "with_session_id": _TEST_STORAGE_BASE | {SZ_USER_DATA: {"session_id": SESSION_ID}}, } TEST_STORAGE_NULL: Final[dict[str, _EmptyStoreT | None]] = { @@ -89,15 +84,12 @@ async def test_auth_tokens_null( idx: str, install: str, ) -> None: - """Test loading/saving authentication tokens when no cached tokens in the store.""" + """Test credentials manager when cache is empty.""" hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_NULL[idx]} - async for mock_client in setup_evohome(hass, config, install=install): - # Confirm client was instantiated without tokens, as cache was empty... - assert SZ_REFRESH_TOKEN not in mock_client.call_args.kwargs - assert SZ_ACCESS_TOKEN not in mock_client.call_args.kwargs - assert SZ_ACCESS_TOKEN_EXPIRES not in mock_client.call_args.kwarg + async for _ in setup_evohome(hass, config, install=install): + pass # Confirm the expected tokens were cached to storage... data: _TokenStoreT = hass_storage[DOMAIN]["data"] @@ -120,17 +112,12 @@ async def test_auth_tokens_same( idx: str, install: str, ) -> None: - """Test loading/saving authentication tokens when matching username.""" + """Test credentials manager when cache contains valid data for this user.""" hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_DATA[idx]} - async for mock_client in setup_evohome(hass, config, install=install): - # Confirm client was instantiated with the cached tokens... - assert mock_client.call_args.kwargs[SZ_REFRESH_TOKEN] == REFRESH_TOKEN - assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN] == ACCESS_TOKEN - assert mock_client.call_args.kwargs[ - SZ_ACCESS_TOKEN_EXPIRES - ] == dt_aware_to_naive(ACCESS_TOKEN_EXP_DTM) + async for _ in setup_evohome(hass, config, install=install): + pass # Confirm the expected tokens were cached to storage... data: _TokenStoreT = hass_storage[DOMAIN]["data"] @@ -150,7 +137,7 @@ async def test_auth_tokens_past( idx: str, install: str, ) -> None: - """Test loading/saving authentication tokens with matching username, but expired.""" + """Test credentials manager when cache contains expired data for this user.""" dt_dtm, dt_str = dt_pair(dt_util.now() - timedelta(hours=1)) @@ -160,19 +147,14 @@ async def test_auth_tokens_past( hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": test_data} - async for mock_client in setup_evohome(hass, config, install=install): - # Confirm client was instantiated with the cached tokens... - assert mock_client.call_args.kwargs[SZ_REFRESH_TOKEN] == REFRESH_TOKEN - assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN] == ACCESS_TOKEN - assert mock_client.call_args.kwargs[ - SZ_ACCESS_TOKEN_EXPIRES - ] == dt_aware_to_naive(dt_dtm) + async for _ in setup_evohome(hass, config, install=install): + pass # Confirm the expected tokens were cached to storage... data: _TokenStoreT = hass_storage[DOMAIN]["data"] assert data[SZ_USERNAME] == USERNAME_SAME - assert data[SZ_REFRESH_TOKEN] == REFRESH_TOKEN + assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}" assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}" assert ( dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True) @@ -189,17 +171,13 @@ async def test_auth_tokens_diff( idx: str, install: str, ) -> None: - """Test loading/saving authentication tokens when unmatched username.""" + """Test credentials manager when cache contains data for a different user.""" hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_STORAGE_DATA[idx]} + config["username"] = USERNAME_DIFF - async for mock_client in setup_evohome( - hass, config | {CONF_USERNAME: USERNAME_DIFF}, install=install - ): - # Confirm client was instantiated without tokens, as username was different... - assert SZ_REFRESH_TOKEN not in mock_client.call_args.kwargs - assert SZ_ACCESS_TOKEN not in mock_client.call_args.kwargs - assert SZ_ACCESS_TOKEN_EXPIRES not in mock_client.call_args.kwarg + async for _ in setup_evohome(hass, config, install=install): + pass # Confirm the expected tokens were cached to storage... data: _TokenStoreT = hass_storage[DOMAIN]["data"] diff --git a/tests/components/evohome/test_water_heater.py b/tests/components/evohome/test_water_heater.py index 8acfd469b59..a201ff63d1e 100644 --- a/tests/components/evohome/test_water_heater.py +++ b/tests/components/evohome/test_water_heater.py @@ -67,7 +67,7 @@ async def test_set_operation_mode( results = [] # SERVICE_SET_OPERATION_MODE: auto - with patch("evohomeasync2.hotwater.HotWater.reset_mode") as mock_fcn: + with patch("evohomeasync2.hotwater.HotWater.reset") as mock_fcn: await hass.services.async_call( Platform.WATER_HEATER, SERVICE_SET_OPERATION_MODE, @@ -78,12 +78,10 @@ async def test_set_operation_mode( blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() # SERVICE_SET_OPERATION_MODE: off (until next scheduled setpoint) - with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn: + with patch("evohomeasync2.hotwater.HotWater.off") as mock_fcn: await hass.services.async_call( Platform.WATER_HEATER, SERVICE_SET_OPERATION_MODE, @@ -94,14 +92,16 @@ async def test_set_operation_mode( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args == () assert mock_fcn.await_args.kwargs != {} results.append(mock_fcn.await_args.kwargs) # SERVICE_SET_OPERATION_MODE: on (until next scheduled setpoint) - with patch("evohomeasync2.hotwater.HotWater.set_on") as mock_fcn: + with patch("evohomeasync2.hotwater.HotWater.on") as mock_fcn: await hass.services.async_call( Platform.WATER_HEATER, SERVICE_SET_OPERATION_MODE, @@ -112,7 +112,9 @@ async def test_set_operation_mode( blocking=True, ) - assert mock_fcn.await_count == 1 + mock_fcn.assert_awaited_once() + + assert mock_fcn.await_args is not None # mypy hint assert mock_fcn.await_args.args == () assert mock_fcn.await_args.kwargs != {} @@ -126,7 +128,7 @@ async def test_set_away_mode(hass: HomeAssistant, evohome: EvohomeClient) -> Non """Test SERVICE_SET_AWAY_MODE of an evohome DHW zone.""" # set_away_mode: off - with patch("evohomeasync2.hotwater.HotWater.reset_mode") as mock_fcn: + with patch("evohomeasync2.hotwater.HotWater.reset") as mock_fcn: await hass.services.async_call( Platform.WATER_HEATER, SERVICE_SET_AWAY_MODE, @@ -137,12 +139,10 @@ async def test_set_away_mode(hass: HomeAssistant, evohome: EvohomeClient) -> Non blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() # set_away_mode: on - with patch("evohomeasync2.hotwater.HotWater.set_off") as mock_fcn: + with patch("evohomeasync2.hotwater.HotWater.off") as mock_fcn: await hass.services.async_call( Platform.WATER_HEATER, SERVICE_SET_AWAY_MODE, @@ -153,9 +153,7 @@ async def test_set_away_mode(hass: HomeAssistant, evohome: EvohomeClient) -> Non blocking=True, ) - assert mock_fcn.await_count == 1 - assert mock_fcn.await_args.args == () - assert mock_fcn.await_args.kwargs == {} + mock_fcn.assert_awaited_once_with() @pytest.mark.parametrize("install", TEST_INSTALLS_WITH_DHW) diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index 6ce17261bfc..09957fe496f 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -67,6 +67,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock]: flexit_bacnet.air_filter_polluted = False flexit_bacnet.air_filter_exchange_interval = 8784 flexit_bacnet.electric_heater = True + flexit_bacnet.fireplace_mode_runtime = 10 # Mock fan setpoints flexit_bacnet.fan_setpoint_extract_air_fire = 56 diff --git a/tests/components/flexit_bacnet/snapshots/test_number.ambr b/tests/components/flexit_bacnet/snapshots/test_number.ambr index 78eefd08345..e2875c140cc 100644 --- a/tests/components/flexit_bacnet/snapshots/test_number.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_number.ambr @@ -284,6 +284,63 @@ 'state': '56', }) # --- +# name: test_numbers[number.device_name_fireplace_mode_runtime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 360, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.device_name_fireplace_mode_runtime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fireplace mode runtime', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fireplace_mode_runtime', + 'unique_id': '0000-0001-fireplace_mode_runtime', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[number.device_name_fireplace_mode_runtime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Device Name Fireplace mode runtime', + 'max': 360, + 'min': 1, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.device_name_fireplace_mode_runtime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- # name: test_numbers[number.device_name_fireplace_supply_fan_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/flexit_bacnet/snapshots/test_switch.ambr b/tests/components/flexit_bacnet/snapshots/test_switch.ambr index d054608f1f7..1df1c12e791 100644 --- a/tests/components/flexit_bacnet/snapshots/test_switch.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_switch.ambr @@ -46,6 +46,53 @@ 'state': 'on', }) # --- +# name: test_switches[switch.device_name_fireplace_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.device_name_fireplace_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fireplace mode', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fireplace_mode', + 'unique_id': '0000-0001-fireplace_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.device_name_fireplace_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Device Name Fireplace mode', + }), + 'context': , + 'entity_id': 'switch.device_name_fireplace_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switches_implementation[switch.device_name_electric_heater-state] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/foscam/test_init.py b/tests/components/foscam/test_init.py index 0b82ed3b02a..a7b6a8c8f0b 100644 --- a/tests/components/foscam/test_init.py +++ b/tests/components/foscam/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant.components.foscam import DOMAIN, config_flow +from homeassistant.components.foscam.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -18,9 +18,7 @@ async def test_unique_id_new_entry( entity_registry: er.EntityRegistry, ) -> None: """Test unique ID for a newly added device is correct.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID - ) + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID) entry.add_to_hass(hass) with ( @@ -46,7 +44,7 @@ async def test_switch_unique_id_migration_ok( ) -> None: """Test that the unique ID for a sleep switch is migrated to the new format.""" entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID, version=1 + domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID, version=1 ) entry.add_to_hass(hass) @@ -57,7 +55,7 @@ async def test_switch_unique_id_migration_ok( # Update config entry with version 2 entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID, version=2 + domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID, version=2 ) entry.add_to_hass(hass) @@ -84,9 +82,7 @@ async def test_unique_id_migration_not_needed( entity_registry: er.EntityRegistry, ) -> None: """Test that the unique ID for a sleep switch is not executed if already in right format.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID - ) + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID) entry.add_to_hass(hass) entity_registry.async_get_or_create( 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/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 21458abb7c8..c89981e67bb 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -244,7 +244,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', @@ -296,7 +296,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', @@ -348,7 +348,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', @@ -401,7 +401,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', @@ -454,7 +454,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', @@ -565,7 +565,7 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr index 316bf74b72a..b445499ad49 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr @@ -5,7 +5,7 @@ 'api_key': '**REDACTED**', }), 'options': dict({ - 'chat_model': 'models/gemini-1.5-flash-latest', + 'chat_model': 'models/gemini-2.0-flash', 'dangerous_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', 'harassment_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', 'hate_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index f68f4c6bf14..c9e02a6d009 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -6,7 +6,7 @@ tuple( ), dict({ - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', }), ), tuple( @@ -32,7 +32,7 @@ tuple( ), dict({ - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-2.0-flash', }), ), tuple( diff --git a/tests/components/google_generative_ai_conversation/test_config_flow.py b/tests/components/google_generative_ai_conversation/test_config_flow.py index d4992c732e1..ee5291196c3 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -39,6 +39,12 @@ from tests.common import MockConfigEntry @pytest.fixture def mock_models(): """Mock the model list API.""" + model_20_flash = Mock( + display_name="Gemini 2.0 Flash", + supported_generation_methods=["generateContent"], + ) + model_20_flash.name = "models/gemini-2.0-flash" + model_15_flash = Mock( display_name="Gemini 1.5 Flash", supported_generation_methods=["generateContent"], @@ -58,7 +64,7 @@ def mock_models(): model_10_pro.name = "models/gemini-pro" with patch( "homeassistant.components.google_generative_ai_conversation.config_flow.genai.list_models", - return_value=iter([model_15_flash, model_15_pro, model_10_pro]), + return_value=iter([model_20_flash, model_15_flash, model_15_pro, model_10_pro]), ): yield diff --git a/tests/components/habitica/snapshots/test_diagnostics.ambr b/tests/components/habitica/snapshots/test_diagnostics.ambr index 1f3a14fade1..2fe3513a646 100644 --- a/tests/components/habitica/snapshots/test_diagnostics.ambr +++ b/tests/components/habitica/snapshots/test_diagnostics.ambr @@ -8,7 +8,6 @@ 'habitica_data': dict({ 'tasks': list([ dict({ - 'Type': 'habit', 'alias': None, 'attribute': 'str', 'byHabitica': False, @@ -71,6 +70,7 @@ 'tags': list([ ]), 'text': 'task text', + 'type': 'habit', 'up': True, 'updatedAt': '2024-10-10T15:57:14.287000+00:00', 'userId': 'ffce870c-3ff3-4fa4-bad1-87612e52b8e7', @@ -80,7 +80,6 @@ 'yesterDaily': None, }), dict({ - 'Type': 'todo', 'alias': None, 'attribute': 'str', 'byHabitica': True, @@ -143,6 +142,7 @@ 'tags': list([ ]), 'text': 'task text', + 'type': 'todo', 'up': None, 'updatedAt': '2024-11-27T19:34:29.001000+00:00', 'userId': 'ffce870c-3ff3-4fa4-bad1-87612e52b8e7', @@ -152,7 +152,6 @@ 'yesterDaily': None, }), dict({ - 'Type': 'reward', 'alias': None, 'attribute': 'str', 'byHabitica': False, @@ -215,6 +214,7 @@ 'tags': list([ ]), 'text': 'task text', + 'type': 'reward', 'up': None, 'updatedAt': '2024-10-10T15:57:14.290000+00:00', 'userId': 'ffce870c-3ff3-4fa4-bad1-87612e52b8e7', @@ -224,7 +224,6 @@ 'yesterDaily': None, }), dict({ - 'Type': 'daily', 'alias': None, 'attribute': 'str', 'byHabitica': False, @@ -341,6 +340,7 @@ 'tags': list([ ]), 'text': 'task text', + 'type': 'daily', 'up': None, 'updatedAt': '2024-11-27T19:34:29.001000+00:00', 'userId': 'ffce870c-3ff3-4fa4-bad1-87612e52b8e7', diff --git a/tests/components/habitica/snapshots/test_services.ambr b/tests/components/habitica/snapshots/test_services.ambr index f40d50ded98..e25ed8db313 100644 --- a/tests/components/habitica/snapshots/test_services.ambr +++ b/tests/components/habitica/snapshots/test_services.ambr @@ -3,9 +3,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -20,13 +19,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -44,12 +43,12 @@ }), 'history': list([ ]), - 'id': UUID('f21fa608-cfc6-4413-9fc7-0eb1b48ca43a'), + 'id': 'f21fa608-cfc6-4413-9fc7-0eb1b48ca43a', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -66,18 +65,18 @@ 'tags': list([ ]), 'text': 'Gesundes Essen/Junkfood', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.268000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -92,13 +91,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -117,19 +116,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 324000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.324000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('1d147de6-5c02-4740-8e2f-71d3015a37f4'), + 'id': '1d147de6-5c02-4740-8e2f-71d3015a37f4', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -146,18 +145,18 @@ 'tags': list([ ]), 'text': 'Eine kurze Pause machen', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -172,13 +171,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.265000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -196,12 +195,12 @@ }), 'history': list([ ]), - 'id': UUID('bc1d1855-b2b8-4663-98ff-62e7b763dfc4'), + 'id': 'bc1d1855-b2b8-4663-98ff-62e7b763dfc4', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Oder lösche es über die Bearbeitungs-Ansicht', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -218,18 +217,18 @@ 'tags': list([ ]), 'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest', + 'type': 'habit', 'up': False, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.265000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'create_a_task', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -244,13 +243,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 264000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.264000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -269,19 +268,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 140000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.140000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('e97659e0-2c42-4599-a7bb-00282adc410d'), + 'id': 'e97659e0-2c42-4599-a7bb-00282adc410d', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -298,18 +297,18 @@ 'tags': list([ ]), 'text': 'Füge eine Aufgabe zu Habitica hinzu', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -321,7 +320,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -329,13 +328,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -354,7 +353,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -362,7 +361,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -370,7 +369,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -378,7 +377,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -386,7 +385,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -394,7 +393,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -402,7 +401,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -410,7 +409,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -418,7 +417,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -426,7 +425,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -434,25 +433,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -464,23 +463,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -495,13 +494,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -520,7 +519,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 17, 55, 3, 74000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T17:55:03.074000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -528,7 +527,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 291000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.291000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -536,7 +535,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 717000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.717000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -544,7 +543,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 7, 20, 59, 722000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T07:20:59.722000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -552,7 +551,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 246000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.246000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -560,7 +559,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 10, 1, 32, 219000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T10:01:32.219000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -568,7 +567,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 691000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.691000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -576,7 +575,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 638000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.638000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -584,7 +583,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 540000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.540000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -592,30 +591,30 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 607000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.607000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -1.919611992979862, }), ]), - 'id': UUID('f2c85972-1a19-4426-bc6d-ce3337b9d99f'), + 'id': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 22, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 23, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 25, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 26, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-22T22:00:00+00:00', + '2024-09-23T22:00:00+00:00', + '2024-09-24T22:00:00+00:00', + '2024-09-25T22:00:00+00:00', + '2024-09-26T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', ]), 'notes': 'Klicke um Deinen Terminplan festzulegen!', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('1491d640-6b21-4d0c-8940-0b7aa61c8836'), + 'id': '1491d640-6b21-4d0c-8940-0b7aa61c8836', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 20, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T20:00:00+00:00', }), ]), 'repeat': dict({ @@ -627,23 +626,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 0, 'tags': list([ ]), 'text': '5 Minuten ruhig durchatmen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 51, 41, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:51:41.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -1.919611992979862, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -655,7 +654,7 @@ 'checklist': list([ dict({ 'completed': True, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -663,13 +662,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-22T11:44:43.774000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -687,18 +686,18 @@ }), 'history': list([ ]), - 'id': UUID('2c6d136c-a1c3-4bef-b7c4-fa980784b1e1'), + 'id': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 28, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 1, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 4, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 8, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-24T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', + '2024-09-28T22:00:00+00:00', + '2024-10-01T22:00:00+00:00', + '2024-10-04T22:00:00+00:00', + '2024-10-08T22:00:00+00:00', ]), 'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -710,24 +709,24 @@ 'th': False, 'w': True, }), - 'startDate': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-21T22:00:00+00:00', 'streak': 0, 'tags': list([ - UUID('6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab'), + '6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab', ]), 'text': 'Fitnessstudio besuchen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), - 'userId': UUID('1343a9af-d891-4027-841a-956d105ca408'), + 'updatedAt': '2024-09-22T11:44:43.774000+00:00', + 'userId': '1343a9af-d891-4027-841a-956d105ca408', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -742,8 +741,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 27, 22, 17, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:57.816000+00:00', + 'date': '2024-09-27T22:17:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -766,12 +765,12 @@ }), 'history': list([ ]), - 'id': UUID('88de7cd9-af2b-49ce-9afd-bf941d87336b'), + 'id': '88de7cd9-af2b-49ce-9afd-bf941d87336b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -786,22 +785,22 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('20409521-c096-447f-9a90-23e8da615710'), - UUID('8515e4ae-2f4b-455a-b4a4-8939e04b1bfd'), + '20409521-c096-447f-9a90-23e8da615710', + '8515e4ae-2f4b-455a-b4a4-8939e04b1bfd', ]), 'text': 'Buch zu Ende lesen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:17:57.816000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'pay_bills', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -816,8 +815,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 19, 513000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 8, 31, 22, 16, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:19.513000+00:00', + 'date': '2024-08-31T22:16:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -840,17 +839,17 @@ }), 'history': list([ ]), - 'id': UUID('2f6fcabc-f670-4ec3-ba65-817e8deea490'), + 'id': '2f6fcabc-f670-4ec3-ba65-817e8deea490', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Strom- und Internetrechnungen rechtzeitig überweisen.', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('91c09432-10ac-4a49-bd20-823081ec29ed'), + 'id': '91c09432-10ac-4a49-bd20-823081ec29ed', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 2, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T02:00:00+00:00', }), ]), 'repeat': dict({ @@ -867,18 +866,18 @@ 'tags': list([ ]), 'text': 'Rechnungen bezahlen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 35, 576000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:35.576000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -893,7 +892,7 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:38.153000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -917,12 +916,12 @@ }), 'history': list([ ]), - 'id': UUID('1aa3137e-ef72-4d1f-91ee-41933602f438'), + 'id': '1aa3137e-ef72-4d1f-91ee-41933602f438', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Rasen mähen und die Pflanzen gießen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -939,18 +938,18 @@ 'tags': list([ ]), 'text': 'Garten pflegen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:38.153000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -965,8 +964,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:16.756000+00:00', + 'date': '2024-09-21T22:00:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -989,12 +988,12 @@ }), 'history': list([ ]), - 'id': UUID('86ea2475-d1b5-4020-bdcc-c188c7996afa'), + 'id': '86ea2475-d1b5-4020-bdcc-c188c7996afa', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Den Ausflug für das kommende Wochenende organisieren.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1009,21 +1008,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('51076966-2970-4b40-b6ba-d58c6a756dd7'), + '51076966-2970-4b40-b6ba-d58c6a756dd7', ]), 'text': 'Wochenendausflug planen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:16.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1038,7 +1037,7 @@ 'completed': None, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -1062,12 +1061,12 @@ }), 'history': list([ ]), - 'id': UUID('5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b'), + 'id': '5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Schaue fern, spiele ein Spiel, gönne Dir einen Leckerbissen, es liegt ganz bei Dir!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1084,18 +1083,18 @@ 'tags': list([ ]), 'text': 'Belohne Dich selbst', + 'type': 'reward', 'up': None, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.266000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 10.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1110,13 +1109,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'monthly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1134,18 +1133,18 @@ }), 'history': list([ ]), - 'id': UUID('6e53f1f5-a315-4edd-984d-8d762e4a08ef'), + 'id': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1157,14 +1156,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Arbeite an einem kreativen Projekt', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ 3, @@ -1172,9 +1172,8 @@ 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1189,13 +1188,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1213,18 +1212,18 @@ }), 'history': list([ ]), - 'id': UUID('7d92278b-9361-4854-83b6-0a66b57dce20'), + 'id': '7d92278b-9361-4854-83b6-0a66b57dce20', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1236,23 +1235,23 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Lerne eine neue Programmiersprache', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1267,7 +1266,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 19, 10, 919000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:19:10.919000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -1291,12 +1290,12 @@ }), 'history': list([ ]), - 'id': UUID('162f0bbe-a097-4a06-b4f4-8fbeed85d2ba'), + 'id': '162f0bbe-a097-4a06-b4f4-8fbeed85d2ba', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Lebensmittel und Haushaltsbedarf für die Woche einkaufen.', - 'priority': , + 'priority': 1.5, 'reminders': list([ ]), 'repeat': dict({ @@ -1311,21 +1310,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wocheneinkauf erledigen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 15, 484000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:15.484000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1340,7 +1339,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 18, 30, 646000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:18:30.646000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -1364,12 +1363,12 @@ }), 'history': list([ ]), - 'id': UUID('3fa06743-aa0f-472b-af1a-f27c755e329c'), + 'id': '3fa06743-aa0f-472b-af1a-f27c755e329c', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Wohnzimmer und Küche gründlich aufräumen.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -1384,12 +1383,13 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wohnung aufräumen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 18, 34, 663000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:18:34.663000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), @@ -1402,9 +1402,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1416,7 +1415,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -1424,13 +1423,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1449,7 +1448,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1457,7 +1456,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1465,7 +1464,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1473,7 +1472,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1481,7 +1480,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1489,7 +1488,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1497,7 +1496,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1505,7 +1504,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1513,7 +1512,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1521,7 +1520,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1529,25 +1528,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1559,14 +1558,15 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), @@ -1579,9 +1579,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1596,13 +1595,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.265000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1620,12 +1619,12 @@ }), 'history': list([ ]), - 'id': UUID('bc1d1855-b2b8-4663-98ff-62e7b763dfc4'), + 'id': 'bc1d1855-b2b8-4663-98ff-62e7b763dfc4', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Oder lösche es über die Bearbeitungs-Ansicht', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1642,18 +1641,18 @@ 'tags': list([ ]), 'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest', + 'type': 'habit', 'up': False, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.265000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'create_a_task', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1668,13 +1667,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 264000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.264000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1693,19 +1692,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 140000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.140000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('e97659e0-2c42-4599-a7bb-00282adc410d'), + 'id': 'e97659e0-2c42-4599-a7bb-00282adc410d', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1722,9 +1721,10 @@ 'tags': list([ ]), 'text': 'Füge eine Aufgabe zu Habitica hinzu', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), @@ -1737,9 +1737,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1751,7 +1750,7 @@ 'checklist': list([ dict({ 'completed': True, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -1759,13 +1758,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-22T11:44:43.774000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1783,18 +1782,18 @@ }), 'history': list([ ]), - 'id': UUID('2c6d136c-a1c3-4bef-b7c4-fa980784b1e1'), + 'id': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 28, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 1, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 4, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 8, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-24T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', + '2024-09-28T22:00:00+00:00', + '2024-10-01T22:00:00+00:00', + '2024-10-04T22:00:00+00:00', + '2024-10-08T22:00:00+00:00', ]), 'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -1806,24 +1805,24 @@ 'th': False, 'w': True, }), - 'startDate': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-21T22:00:00+00:00', 'streak': 0, 'tags': list([ - UUID('6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab'), + '6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab', ]), 'text': 'Fitnessstudio besuchen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), - 'userId': UUID('1343a9af-d891-4027-841a-956d105ca408'), + 'updatedAt': '2024-09-22T11:44:43.774000+00:00', + 'userId': '1343a9af-d891-4027-841a-956d105ca408', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1838,8 +1837,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 27, 22, 17, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:57.816000+00:00', + 'date': '2024-09-27T22:17:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -1862,12 +1861,12 @@ }), 'history': list([ ]), - 'id': UUID('88de7cd9-af2b-49ce-9afd-bf941d87336b'), + 'id': '88de7cd9-af2b-49ce-9afd-bf941d87336b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -1882,13 +1881,14 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('20409521-c096-447f-9a90-23e8da615710'), - UUID('8515e4ae-2f4b-455a-b4a4-8939e04b1bfd'), + '20409521-c096-447f-9a90-23e8da615710', + '8515e4ae-2f4b-455a-b4a4-8939e04b1bfd', ]), 'text': 'Buch zu Ende lesen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:17:57.816000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), @@ -1901,9 +1901,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -1918,13 +1917,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -1943,7 +1942,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 17, 55, 3, 74000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T17:55:03.074000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1951,7 +1950,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 291000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.291000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1959,7 +1958,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 717000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.717000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1967,7 +1966,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 7, 20, 59, 722000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T07:20:59.722000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1975,7 +1974,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 246000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.246000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1983,7 +1982,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 10, 1, 32, 219000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T10:01:32.219000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1991,7 +1990,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 691000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.691000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -1999,7 +1998,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 638000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.638000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2007,7 +2006,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 540000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.540000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2015,30 +2014,30 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 607000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.607000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -1.919611992979862, }), ]), - 'id': UUID('f2c85972-1a19-4426-bc6d-ce3337b9d99f'), + 'id': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 22, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 23, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 25, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 26, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-22T22:00:00+00:00', + '2024-09-23T22:00:00+00:00', + '2024-09-24T22:00:00+00:00', + '2024-09-25T22:00:00+00:00', + '2024-09-26T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', ]), 'notes': 'Klicke um Deinen Terminplan festzulegen!', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('1491d640-6b21-4d0c-8940-0b7aa61c8836'), + 'id': '1491d640-6b21-4d0c-8940-0b7aa61c8836', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 20, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T20:00:00+00:00', }), ]), 'repeat': dict({ @@ -2050,14 +2049,15 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 0, 'tags': list([ ]), 'text': '5 Minuten ruhig durchatmen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 51, 41, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:51:41.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -1.919611992979862, 'weeksOfMonth': list([ ]), @@ -2070,9 +2070,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2087,13 +2086,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2112,19 +2111,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 324000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.324000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('1d147de6-5c02-4740-8e2f-71d3015a37f4'), + 'id': '1d147de6-5c02-4740-8e2f-71d3015a37f4', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2141,18 +2140,18 @@ 'tags': list([ ]), 'text': 'Eine kurze Pause machen', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2164,7 +2163,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -2172,13 +2171,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2197,7 +2196,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2205,7 +2204,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2213,7 +2212,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2221,7 +2220,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2229,7 +2228,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2237,7 +2236,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2245,7 +2244,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2253,7 +2252,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2261,7 +2260,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2269,7 +2268,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2277,25 +2276,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2307,14 +2306,15 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), @@ -2327,9 +2327,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2344,13 +2343,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2368,12 +2367,12 @@ }), 'history': list([ ]), - 'id': UUID('f21fa608-cfc6-4413-9fc7-0eb1b48ca43a'), + 'id': 'f21fa608-cfc6-4413-9fc7-0eb1b48ca43a', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2390,18 +2389,18 @@ 'tags': list([ ]), 'text': 'Gesundes Essen/Junkfood', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.268000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2416,13 +2415,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2441,19 +2440,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 324000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.324000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('1d147de6-5c02-4740-8e2f-71d3015a37f4'), + 'id': '1d147de6-5c02-4740-8e2f-71d3015a37f4', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2470,18 +2469,18 @@ 'tags': list([ ]), 'text': 'Eine kurze Pause machen', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2496,13 +2495,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.265000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2520,12 +2519,12 @@ }), 'history': list([ ]), - 'id': UUID('bc1d1855-b2b8-4663-98ff-62e7b763dfc4'), + 'id': 'bc1d1855-b2b8-4663-98ff-62e7b763dfc4', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Oder lösche es über die Bearbeitungs-Ansicht', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2542,18 +2541,18 @@ 'tags': list([ ]), 'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest', + 'type': 'habit', 'up': False, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.265000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'create_a_task', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2568,13 +2567,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 264000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.264000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2593,19 +2592,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 140000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.140000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('e97659e0-2c42-4599-a7bb-00282adc410d'), + 'id': 'e97659e0-2c42-4599-a7bb-00282adc410d', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2622,18 +2621,18 @@ 'tags': list([ ]), 'text': 'Füge eine Aufgabe zu Habitica hinzu', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2645,7 +2644,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -2653,13 +2652,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2678,7 +2677,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2686,7 +2685,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2694,7 +2693,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2702,7 +2701,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2710,7 +2709,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2718,7 +2717,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2726,7 +2725,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2734,7 +2733,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2742,7 +2741,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2750,7 +2749,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2758,25 +2757,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -2788,23 +2787,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2819,13 +2818,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -2844,7 +2843,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 17, 55, 3, 74000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T17:55:03.074000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2852,7 +2851,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 291000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.291000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2860,7 +2859,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 717000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.717000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2868,7 +2867,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 7, 20, 59, 722000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T07:20:59.722000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2876,7 +2875,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 246000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.246000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2884,7 +2883,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 10, 1, 32, 219000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T10:01:32.219000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2892,7 +2891,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 691000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.691000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2900,7 +2899,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 638000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.638000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2908,7 +2907,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 540000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.540000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -2916,30 +2915,30 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 607000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.607000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -1.919611992979862, }), ]), - 'id': UUID('f2c85972-1a19-4426-bc6d-ce3337b9d99f'), + 'id': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 22, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 23, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 25, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 26, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-22T22:00:00+00:00', + '2024-09-23T22:00:00+00:00', + '2024-09-24T22:00:00+00:00', + '2024-09-25T22:00:00+00:00', + '2024-09-26T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', ]), 'notes': 'Klicke um Deinen Terminplan festzulegen!', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('1491d640-6b21-4d0c-8940-0b7aa61c8836'), + 'id': '1491d640-6b21-4d0c-8940-0b7aa61c8836', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 20, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T20:00:00+00:00', }), ]), 'repeat': dict({ @@ -2951,23 +2950,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 0, 'tags': list([ ]), 'text': '5 Minuten ruhig durchatmen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 51, 41, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:51:41.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -1.919611992979862, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -2982,8 +2981,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 27, 22, 17, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:57.816000+00:00', + 'date': '2024-09-27T22:17:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -3006,12 +3005,12 @@ }), 'history': list([ ]), - 'id': UUID('88de7cd9-af2b-49ce-9afd-bf941d87336b'), + 'id': '88de7cd9-af2b-49ce-9afd-bf941d87336b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3026,22 +3025,22 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('20409521-c096-447f-9a90-23e8da615710'), - UUID('8515e4ae-2f4b-455a-b4a4-8939e04b1bfd'), + '20409521-c096-447f-9a90-23e8da615710', + '8515e4ae-2f4b-455a-b4a4-8939e04b1bfd', ]), 'text': 'Buch zu Ende lesen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:17:57.816000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'pay_bills', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3056,8 +3055,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 19, 513000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 8, 31, 22, 16, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:19.513000+00:00', + 'date': '2024-08-31T22:16:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -3080,17 +3079,17 @@ }), 'history': list([ ]), - 'id': UUID('2f6fcabc-f670-4ec3-ba65-817e8deea490'), + 'id': '2f6fcabc-f670-4ec3-ba65-817e8deea490', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Strom- und Internetrechnungen rechtzeitig überweisen.', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('91c09432-10ac-4a49-bd20-823081ec29ed'), + 'id': '91c09432-10ac-4a49-bd20-823081ec29ed', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 2, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T02:00:00+00:00', }), ]), 'repeat': dict({ @@ -3107,18 +3106,18 @@ 'tags': list([ ]), 'text': 'Rechnungen bezahlen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 35, 576000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:35.576000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3133,7 +3132,7 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:38.153000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -3157,12 +3156,12 @@ }), 'history': list([ ]), - 'id': UUID('1aa3137e-ef72-4d1f-91ee-41933602f438'), + 'id': '1aa3137e-ef72-4d1f-91ee-41933602f438', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Rasen mähen und die Pflanzen gießen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3179,18 +3178,18 @@ 'tags': list([ ]), 'text': 'Garten pflegen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:38.153000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3205,8 +3204,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:16.756000+00:00', + 'date': '2024-09-21T22:00:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -3229,12 +3228,12 @@ }), 'history': list([ ]), - 'id': UUID('86ea2475-d1b5-4020-bdcc-c188c7996afa'), + 'id': '86ea2475-d1b5-4020-bdcc-c188c7996afa', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Den Ausflug für das kommende Wochenende organisieren.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3249,21 +3248,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('51076966-2970-4b40-b6ba-d58c6a756dd7'), + '51076966-2970-4b40-b6ba-d58c6a756dd7', ]), 'text': 'Wochenendausflug planen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:16.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3278,7 +3277,7 @@ 'completed': None, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -3302,12 +3301,12 @@ }), 'history': list([ ]), - 'id': UUID('5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b'), + 'id': '5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Schaue fern, spiele ein Spiel, gönne Dir einen Leckerbissen, es liegt ganz bei Dir!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3324,18 +3323,18 @@ 'tags': list([ ]), 'text': 'Belohne Dich selbst', + 'type': 'reward', 'up': None, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.266000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 10.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3350,13 +3349,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'monthly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3374,18 +3373,18 @@ }), 'history': list([ ]), - 'id': UUID('6e53f1f5-a315-4edd-984d-8d762e4a08ef'), + 'id': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3397,14 +3396,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Arbeite an einem kreativen Projekt', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ 3, @@ -3412,9 +3412,8 @@ 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3429,13 +3428,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3453,18 +3452,18 @@ }), 'history': list([ ]), - 'id': UUID('7d92278b-9361-4854-83b6-0a66b57dce20'), + 'id': '7d92278b-9361-4854-83b6-0a66b57dce20', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3476,14 +3475,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Lerne eine neue Programmiersprache', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ ]), @@ -3502,9 +3502,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3516,7 +3515,7 @@ 'checklist': list([ dict({ 'completed': True, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -3524,13 +3523,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-22T11:44:43.774000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3548,18 +3547,18 @@ }), 'history': list([ ]), - 'id': UUID('2c6d136c-a1c3-4bef-b7c4-fa980784b1e1'), + 'id': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 28, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 1, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 4, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 8, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-24T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', + '2024-09-28T22:00:00+00:00', + '2024-10-01T22:00:00+00:00', + '2024-10-04T22:00:00+00:00', + '2024-10-08T22:00:00+00:00', ]), 'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -3571,24 +3570,24 @@ 'th': False, 'w': True, }), - 'startDate': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-21T22:00:00+00:00', 'streak': 0, 'tags': list([ - UUID('6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab'), + '6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab', ]), 'text': 'Fitnessstudio besuchen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), - 'userId': UUID('1343a9af-d891-4027-841a-956d105ca408'), + 'updatedAt': '2024-09-22T11:44:43.774000+00:00', + 'userId': '1343a9af-d891-4027-841a-956d105ca408', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3603,7 +3602,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 18, 30, 646000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:18:30.646000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -3627,12 +3626,12 @@ }), 'history': list([ ]), - 'id': UUID('3fa06743-aa0f-472b-af1a-f27c755e329c'), + 'id': '3fa06743-aa0f-472b-af1a-f27c755e329c', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Wohnzimmer und Küche gründlich aufräumen.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -3647,12 +3646,13 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wohnung aufräumen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 18, 34, 663000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:18:34.663000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), @@ -3665,9 +3665,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3682,7 +3681,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 19, 10, 919000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:19:10.919000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -3706,12 +3705,12 @@ }), 'history': list([ ]), - 'id': UUID('162f0bbe-a097-4a06-b4f4-8fbeed85d2ba'), + 'id': '162f0bbe-a097-4a06-b4f4-8fbeed85d2ba', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Lebensmittel und Haushaltsbedarf für die Woche einkaufen.', - 'priority': , + 'priority': 1.5, 'reminders': list([ ]), 'repeat': dict({ @@ -3726,12 +3725,13 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wocheneinkauf erledigen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 15, 484000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:15.484000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), @@ -3744,9 +3744,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3761,13 +3760,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3785,12 +3784,12 @@ }), 'history': list([ ]), - 'id': UUID('f21fa608-cfc6-4413-9fc7-0eb1b48ca43a'), + 'id': 'f21fa608-cfc6-4413-9fc7-0eb1b48ca43a', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3807,18 +3806,18 @@ 'tags': list([ ]), 'text': 'Gesundes Essen/Junkfood', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.268000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3833,13 +3832,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3858,19 +3857,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 324000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.324000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('1d147de6-5c02-4740-8e2f-71d3015a37f4'), + 'id': '1d147de6-5c02-4740-8e2f-71d3015a37f4', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3887,18 +3886,18 @@ 'tags': list([ ]), 'text': 'Eine kurze Pause machen', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3913,13 +3912,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.265000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -3937,12 +3936,12 @@ }), 'history': list([ ]), - 'id': UUID('bc1d1855-b2b8-4663-98ff-62e7b763dfc4'), + 'id': 'bc1d1855-b2b8-4663-98ff-62e7b763dfc4', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Oder lösche es über die Bearbeitungs-Ansicht', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -3959,18 +3958,18 @@ 'tags': list([ ]), 'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest', + 'type': 'habit', 'up': False, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.265000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'create_a_task', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -3985,13 +3984,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 264000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.264000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4010,19 +4009,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 140000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.140000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('e97659e0-2c42-4599-a7bb-00282adc410d'), + 'id': 'e97659e0-2c42-4599-a7bb-00282adc410d', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -4039,18 +4038,18 @@ 'tags': list([ ]), 'text': 'Füge eine Aufgabe zu Habitica hinzu', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4062,7 +4061,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -4070,13 +4069,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4095,7 +4094,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4103,7 +4102,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4111,7 +4110,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4119,7 +4118,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4127,7 +4126,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4135,7 +4134,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4143,7 +4142,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4151,7 +4150,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4159,7 +4158,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4167,7 +4166,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4175,25 +4174,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -4205,23 +4204,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4236,13 +4235,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4261,7 +4260,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 17, 55, 3, 74000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T17:55:03.074000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4269,7 +4268,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 291000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.291000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4277,7 +4276,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 717000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.717000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4285,7 +4284,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 7, 20, 59, 722000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T07:20:59.722000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4293,7 +4292,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 246000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.246000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4301,7 +4300,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 10, 1, 32, 219000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T10:01:32.219000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4309,7 +4308,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 691000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.691000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4317,7 +4316,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 638000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.638000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4325,7 +4324,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 540000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.540000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4333,30 +4332,30 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 607000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.607000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -1.919611992979862, }), ]), - 'id': UUID('f2c85972-1a19-4426-bc6d-ce3337b9d99f'), + 'id': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 22, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 23, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 25, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 26, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-22T22:00:00+00:00', + '2024-09-23T22:00:00+00:00', + '2024-09-24T22:00:00+00:00', + '2024-09-25T22:00:00+00:00', + '2024-09-26T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', ]), 'notes': 'Klicke um Deinen Terminplan festzulegen!', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('1491d640-6b21-4d0c-8940-0b7aa61c8836'), + 'id': '1491d640-6b21-4d0c-8940-0b7aa61c8836', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 20, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T20:00:00+00:00', }), ]), 'repeat': dict({ @@ -4368,23 +4367,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 0, 'tags': list([ ]), 'text': '5 Minuten ruhig durchatmen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 51, 41, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:51:41.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -1.919611992979862, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4396,7 +4395,7 @@ 'checklist': list([ dict({ 'completed': True, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -4404,13 +4403,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-22T11:44:43.774000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4428,18 +4427,18 @@ }), 'history': list([ ]), - 'id': UUID('2c6d136c-a1c3-4bef-b7c4-fa980784b1e1'), + 'id': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 28, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 1, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 4, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 8, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-24T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', + '2024-09-28T22:00:00+00:00', + '2024-10-01T22:00:00+00:00', + '2024-10-04T22:00:00+00:00', + '2024-10-08T22:00:00+00:00', ]), 'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -4451,24 +4450,24 @@ 'th': False, 'w': True, }), - 'startDate': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-21T22:00:00+00:00', 'streak': 0, 'tags': list([ - UUID('6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab'), + '6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab', ]), 'text': 'Fitnessstudio besuchen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), - 'userId': UUID('1343a9af-d891-4027-841a-956d105ca408'), + 'updatedAt': '2024-09-22T11:44:43.774000+00:00', + 'userId': '1343a9af-d891-4027-841a-956d105ca408', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4483,13 +4482,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'monthly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4507,18 +4506,18 @@ }), 'history': list([ ]), - 'id': UUID('6e53f1f5-a315-4edd-984d-8d762e4a08ef'), + 'id': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -4530,14 +4529,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Arbeite an einem kreativen Projekt', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ 3, @@ -4545,9 +4545,8 @@ 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4562,13 +4561,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4586,18 +4585,18 @@ }), 'history': list([ ]), - 'id': UUID('7d92278b-9361-4854-83b6-0a66b57dce20'), + 'id': '7d92278b-9361-4854-83b6-0a66b57dce20', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -4609,14 +4608,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Lerne eine neue Programmiersprache', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ ]), @@ -4629,9 +4629,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': 'alias_zahnseide_benutzen', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4643,7 +4642,7 @@ 'checklist': list([ dict({ 'completed': False, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -4651,13 +4650,13 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4676,7 +4675,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 6, 749000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:06.749000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4684,7 +4683,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 292000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.292000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4692,7 +4691,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 719000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.719000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4700,7 +4699,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 9, 44, 56, 907000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T09:44:56.907000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4708,7 +4707,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 243000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.243000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4716,7 +4715,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 20, 20, 19, 56, 447000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-20T20:19:56.447000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4724,7 +4723,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 692000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.692000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4732,7 +4731,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 640000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.640000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4740,7 +4739,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 542000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.542000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4748,7 +4747,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 608000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.608000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4756,25 +4755,25 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 9, 21, 22, 24, 20, 150000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:24:20.150000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -2.9663035443712333, }), ]), - 'id': UUID('564b9ac9-c53d-4638-9e7f-1cd96fe19baa'), + 'id': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 23, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 24, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 25, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 26, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 27, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), - datetime.datetime(2024, 9, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'GMT')), + '2024-09-23T00:00:00+02:00', + '2024-09-24T00:00:00+02:00', + '2024-09-25T00:00:00+02:00', + '2024-09-26T00:00:00+02:00', + '2024-09-27T00:00:00+02:00', + '2024-09-28T00:00:00+02:00', ]), 'notes': 'Klicke um Änderungen zu machen!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -4786,23 +4785,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Zahnseide benutzen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 24, 20, 154000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:24:20.154000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -2.9663035443712333, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4817,13 +4816,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -4842,7 +4841,7 @@ 'history': list([ dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 7, 17, 55, 3, 74000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T17:55:03.074000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4850,7 +4849,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 17, 15, 11, 291000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T17:15:11.291000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4858,7 +4857,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 7, 9, 22, 31, 46, 717000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-09T22:31:46.717000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4866,7 +4865,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 11, 7, 20, 59, 722000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-11T07:20:59.722000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4874,7 +4873,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 9, 58, 45, 246000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T09:58:45.246000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4882,7 +4881,7 @@ }), dict({ 'completed': True, - 'date': datetime.datetime(2024, 7, 12, 10, 1, 32, 219000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-12T10:01:32.219000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4890,7 +4889,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 8, 21, 15, 55, 7, 691000, tzinfo=datetime.timezone.utc), + 'date': '2024-08-21T15:55:07.691000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4898,7 +4897,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 20, 15, 29, 23, 638000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-20T15:29:23.638000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4906,7 +4905,7 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 21, 23, 7, 540000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T21:23:07.540000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, @@ -4914,30 +4913,30 @@ }), dict({ 'completed': False, - 'date': datetime.datetime(2024, 9, 21, 22, 1, 55, 607000, tzinfo=datetime.timezone.utc), + 'date': '2024-09-21T22:01:55.607000+00:00', 'isDue': True, 'scoredDown': None, 'scoredUp': None, 'value': -1.919611992979862, }), ]), - 'id': UUID('f2c85972-1a19-4426-bc6d-ce3337b9d99f'), + 'id': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 22, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 23, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 25, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 26, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-22T22:00:00+00:00', + '2024-09-23T22:00:00+00:00', + '2024-09-24T22:00:00+00:00', + '2024-09-25T22:00:00+00:00', + '2024-09-26T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', ]), 'notes': 'Klicke um Deinen Terminplan festzulegen!', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('1491d640-6b21-4d0c-8940-0b7aa61c8836'), + 'id': '1491d640-6b21-4d0c-8940-0b7aa61c8836', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 20, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T20:00:00+00:00', }), ]), 'repeat': dict({ @@ -4949,23 +4948,23 @@ 'th': True, 'w': True, }), - 'startDate': datetime.datetime(2024, 7, 6, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-07-06T22:00:00+00:00', 'streak': 0, 'tags': list([ ]), 'text': '5 Minuten ruhig durchatmen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 51, 41, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:51:41.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -1.919611992979862, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -4977,7 +4976,7 @@ 'checklist': list([ dict({ 'completed': True, - 'id': UUID('c8662c16-8cd3-4104-a3b2-b1e54f61b8ca'), + 'id': 'c8662c16-8cd3-4104-a3b2-b1e54f61b8ca', 'text': 'Checklist-item1', }), ]), @@ -4985,13 +4984,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-22T11:44:43.774000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5009,18 +5008,18 @@ }), 'history': list([ ]), - 'id': UUID('2c6d136c-a1c3-4bef-b7c4-fa980784b1e1'), + 'id': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'isDue': True, 'nextDue': list([ - datetime.datetime(2024, 9, 24, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 27, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 9, 28, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 1, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 4, 22, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2024, 10, 8, 22, 0, tzinfo=datetime.timezone.utc), + '2024-09-24T22:00:00+00:00', + '2024-09-27T22:00:00+00:00', + '2024-09-28T22:00:00+00:00', + '2024-10-01T22:00:00+00:00', + '2024-10-04T22:00:00+00:00', + '2024-10-08T22:00:00+00:00', ]), 'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -5032,24 +5031,24 @@ 'th': False, 'w': True, }), - 'startDate': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-21T22:00:00+00:00', 'streak': 0, 'tags': list([ - UUID('6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab'), + '6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab', ]), 'text': 'Fitnessstudio besuchen', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 22, 11, 44, 43, 774000, tzinfo=datetime.timezone.utc), - 'userId': UUID('1343a9af-d891-4027-841a-956d105ca408'), + 'updatedAt': '2024-09-22T11:44:43.774000+00:00', + 'userId': '1343a9af-d891-4027-841a-956d105ca408', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5064,13 +5063,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'monthly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5088,18 +5087,18 @@ }), 'history': list([ ]), - 'id': UUID('6e53f1f5-a315-4edd-984d-8d762e4a08ef'), + 'id': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5111,14 +5110,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Arbeite an einem kreativen Projekt', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ 3, @@ -5126,9 +5126,8 @@ 'yesterDaily': True, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5143,13 +5142,13 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 10, 10, 15, 57, 14, 304000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-10-10T15:57:14.304000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': None, 'everyX': 1, - 'frequency': , + 'frequency': 'weekly', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5167,18 +5166,18 @@ }), 'history': list([ ]), - 'id': UUID('7d92278b-9361-4854-83b6-0a66b57dce20'), + 'id': '7d92278b-9361-4854-83b6-0a66b57dce20', 'isDue': False, 'nextDue': list([ - datetime.datetime(2024, 12, 14, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 1, 18, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 2, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 3, 15, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 4, 19, 23, 0, tzinfo=datetime.timezone.utc), - datetime.datetime(2025, 5, 17, 23, 0, tzinfo=datetime.timezone.utc), + '2024-12-14T23:00:00+00:00', + '2025-01-18T23:00:00+00:00', + '2025-02-15T23:00:00+00:00', + '2025-03-15T23:00:00+00:00', + '2025-04-19T23:00:00+00:00', + '2025-05-17T23:00:00+00:00', ]), 'notes': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5190,14 +5189,15 @@ 'th': False, 'w': False, }), - 'startDate': datetime.datetime(2024, 9, 20, 23, 0, tzinfo=datetime.timezone.utc), + 'startDate': '2024-09-20T23:00:00+00:00', 'streak': 1, 'tags': list([ ]), 'text': 'Lerne eine neue Programmiersprache', + 'type': 'daily', 'up': None, - 'updatedAt': datetime.datetime(2024, 11, 27, 23, 47, 29, 986000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-11-27T23:47:29.986000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': -0.9215181434950852, 'weeksOfMonth': list([ ]), @@ -5210,9 +5210,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5227,13 +5226,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.268000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5251,12 +5250,12 @@ }), 'history': list([ ]), - 'id': UUID('f21fa608-cfc6-4413-9fc7-0eb1b48ca43a'), + 'id': 'f21fa608-cfc6-4413-9fc7-0eb1b48ca43a', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5273,18 +5272,18 @@ 'tags': list([ ]), 'text': 'Gesundes Essen/Junkfood', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 268000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.268000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5299,13 +5298,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5324,19 +5323,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 324000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.324000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('1d147de6-5c02-4740-8e2f-71d3015a37f4'), + 'id': '1d147de6-5c02-4740-8e2f-71d3015a37f4', 'isDue': None, 'nextDue': list([ ]), 'notes': '', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5353,18 +5352,18 @@ 'tags': list([ ]), 'text': 'Eine kurze Pause machen', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5379,13 +5378,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.265000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': True, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5403,12 +5402,12 @@ }), 'history': list([ ]), - 'id': UUID('bc1d1855-b2b8-4663-98ff-62e7b763dfc4'), + 'id': 'bc1d1855-b2b8-4663-98ff-62e7b763dfc4', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Oder lösche es über die Bearbeitungs-Ansicht', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5425,18 +5424,18 @@ 'tags': list([ ]), 'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest', + 'type': 'habit', 'up': False, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 265000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.265000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'create_a_task', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5451,13 +5450,13 @@ 'completed': None, 'counterDown': 0, 'counterUp': 0, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 264000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.264000+00:00', 'date': None, 'daysOfMonth': list([ ]), 'down': False, 'everyX': None, - 'frequency': , + 'frequency': 'daily', 'group': dict({ 'assignedDate': None, 'assignedUsers': list([ @@ -5476,19 +5475,19 @@ 'history': list([ dict({ 'completed': None, - 'date': datetime.datetime(2024, 7, 7, 18, 26, 3, 140000, tzinfo=datetime.timezone.utc), + 'date': '2024-07-07T18:26:03.140000+00:00', 'isDue': None, 'scoredDown': 0, 'scoredUp': 1, 'value': 1.0, }), ]), - 'id': UUID('e97659e0-2c42-4599-a7bb-00282adc410d'), + 'id': 'e97659e0-2c42-4599-a7bb-00282adc410d', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5505,9 +5504,10 @@ 'tags': list([ ]), 'text': 'Füge eine Aufgabe zu Habitica hinzu', + 'type': 'habit', 'up': True, - 'updatedAt': datetime.datetime(2024, 7, 12, 9, 58, 45, 438000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-12T09:58:45.438000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), @@ -5520,9 +5520,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5537,7 +5536,7 @@ 'completed': None, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -5561,12 +5560,12 @@ }), 'history': list([ ]), - 'id': UUID('5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b'), + 'id': '5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Schaue fern, spiele ein Spiel, gönne Dir einen Leckerbissen, es liegt ganz bei Dir!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5583,9 +5582,10 @@ 'tags': list([ ]), 'text': 'Belohne Dich selbst', + 'type': 'reward', 'up': None, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.266000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 10.0, 'weeksOfMonth': list([ ]), @@ -5598,9 +5598,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5615,8 +5614,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 27, 22, 17, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:57.816000+00:00', + 'date': '2024-09-27T22:17:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -5639,12 +5638,12 @@ }), 'history': list([ ]), - 'id': UUID('88de7cd9-af2b-49ce-9afd-bf941d87336b'), + 'id': '88de7cd9-af2b-49ce-9afd-bf941d87336b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5659,22 +5658,22 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('20409521-c096-447f-9a90-23e8da615710'), - UUID('8515e4ae-2f4b-455a-b4a4-8939e04b1bfd'), + '20409521-c096-447f-9a90-23e8da615710', + '8515e4ae-2f4b-455a-b4a4-8939e04b1bfd', ]), 'text': 'Buch zu Ende lesen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:17:57.816000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'pay_bills', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5689,8 +5688,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 19, 513000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 8, 31, 22, 16, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:19.513000+00:00', + 'date': '2024-08-31T22:16:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -5713,17 +5712,17 @@ }), 'history': list([ ]), - 'id': UUID('2f6fcabc-f670-4ec3-ba65-817e8deea490'), + 'id': '2f6fcabc-f670-4ec3-ba65-817e8deea490', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Strom- und Internetrechnungen rechtzeitig überweisen.', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('91c09432-10ac-4a49-bd20-823081ec29ed'), + 'id': '91c09432-10ac-4a49-bd20-823081ec29ed', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 2, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T02:00:00+00:00', }), ]), 'repeat': dict({ @@ -5740,18 +5739,18 @@ 'tags': list([ ]), 'text': 'Rechnungen bezahlen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 35, 576000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:35.576000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5766,7 +5765,7 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:38.153000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -5790,12 +5789,12 @@ }), 'history': list([ ]), - 'id': UUID('1aa3137e-ef72-4d1f-91ee-41933602f438'), + 'id': '1aa3137e-ef72-4d1f-91ee-41933602f438', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Rasen mähen und die Pflanzen gießen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5812,18 +5811,18 @@ 'tags': list([ ]), 'text': 'Garten pflegen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:38.153000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5838,8 +5837,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:16.756000+00:00', + 'date': '2024-09-21T22:00:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -5862,12 +5861,12 @@ }), 'history': list([ ]), - 'id': UUID('86ea2475-d1b5-4020-bdcc-c188c7996afa'), + 'id': '86ea2475-d1b5-4020-bdcc-c188c7996afa', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Den Ausflug für das kommende Wochenende organisieren.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5882,21 +5881,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('51076966-2970-4b40-b6ba-d58c6a756dd7'), + '51076966-2970-4b40-b6ba-d58c6a756dd7', ]), 'text': 'Wochenendausflug planen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:16.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5911,7 +5910,7 @@ 'completed': None, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-07-07T17:51:53.266000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -5935,12 +5934,12 @@ }), 'history': list([ ]), - 'id': UUID('5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b'), + 'id': '5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Schaue fern, spiele ein Spiel, gönne Dir einen Leckerbissen, es liegt ganz bei Dir!', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -5957,18 +5956,18 @@ 'tags': list([ ]), 'text': 'Belohne Dich selbst', + 'type': 'reward', 'up': None, - 'updatedAt': datetime.datetime(2024, 7, 7, 17, 51, 53, 266000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-07-07T17:51:53.266000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 10.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -5983,7 +5982,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 19, 10, 919000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:19:10.919000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -6007,12 +6006,12 @@ }), 'history': list([ ]), - 'id': UUID('162f0bbe-a097-4a06-b4f4-8fbeed85d2ba'), + 'id': '162f0bbe-a097-4a06-b4f4-8fbeed85d2ba', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Lebensmittel und Haushaltsbedarf für die Woche einkaufen.', - 'priority': , + 'priority': 1.5, 'reminders': list([ ]), 'repeat': dict({ @@ -6027,21 +6026,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wocheneinkauf erledigen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 15, 484000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:15.484000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6056,7 +6055,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 18, 30, 646000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:18:30.646000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -6080,12 +6079,12 @@ }), 'history': list([ ]), - 'id': UUID('3fa06743-aa0f-472b-af1a-f27c755e329c'), + 'id': '3fa06743-aa0f-472b-af1a-f27c755e329c', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Wohnzimmer und Küche gründlich aufräumen.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -6100,12 +6099,13 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wohnung aufräumen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 18, 34, 663000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:18:34.663000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), @@ -6118,9 +6118,8 @@ dict({ 'tasks': list([ dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6135,8 +6134,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 27, 22, 17, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:57.816000+00:00', + 'date': '2024-09-27T22:17:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -6159,12 +6158,12 @@ }), 'history': list([ ]), - 'id': UUID('88de7cd9-af2b-49ce-9afd-bf941d87336b'), + 'id': '88de7cd9-af2b-49ce-9afd-bf941d87336b', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -6179,22 +6178,22 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('20409521-c096-447f-9a90-23e8da615710'), - UUID('8515e4ae-2f4b-455a-b4a4-8939e04b1bfd'), + '20409521-c096-447f-9a90-23e8da615710', + '8515e4ae-2f4b-455a-b4a4-8939e04b1bfd', ]), 'text': 'Buch zu Ende lesen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 17, 57, 816000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:17:57.816000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': 'pay_bills', - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6209,8 +6208,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 17, 19, 513000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 8, 31, 22, 16, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:17:19.513000+00:00', + 'date': '2024-08-31T22:16:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -6233,17 +6232,17 @@ }), 'history': list([ ]), - 'id': UUID('2f6fcabc-f670-4ec3-ba65-817e8deea490'), + 'id': '2f6fcabc-f670-4ec3-ba65-817e8deea490', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Strom- und Internetrechnungen rechtzeitig überweisen.', - 'priority': , + 'priority': 1, 'reminders': list([ dict({ - 'id': UUID('91c09432-10ac-4a49-bd20-823081ec29ed'), + 'id': '91c09432-10ac-4a49-bd20-823081ec29ed', 'startDate': None, - 'time': datetime.datetime(2024, 9, 22, 2, 0, tzinfo=datetime.timezone.utc), + 'time': '2024-09-22T02:00:00+00:00', }), ]), 'repeat': dict({ @@ -6260,18 +6259,18 @@ 'tags': list([ ]), 'text': 'Rechnungen bezahlen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 35, 576000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:35.576000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6286,7 +6285,7 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:38.153000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -6310,12 +6309,12 @@ }), 'history': list([ ]), - 'id': UUID('1aa3137e-ef72-4d1f-91ee-41933602f438'), + 'id': '1aa3137e-ef72-4d1f-91ee-41933602f438', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Rasen mähen und die Pflanzen gießen.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -6332,18 +6331,18 @@ 'tags': list([ ]), 'text': 'Garten pflegen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 38, 153000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:38.153000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6358,8 +6357,8 @@ 'completed': False, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'date': datetime.datetime(2024, 9, 21, 22, 0, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:16:16.756000+00:00', + 'date': '2024-09-21T22:00:00+00:00', 'daysOfMonth': list([ ]), 'down': None, @@ -6382,12 +6381,12 @@ }), 'history': list([ ]), - 'id': UUID('86ea2475-d1b5-4020-bdcc-c188c7996afa'), + 'id': '86ea2475-d1b5-4020-bdcc-c188c7996afa', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Den Ausflug für das kommende Wochenende organisieren.', - 'priority': , + 'priority': 1, 'reminders': list([ ]), 'repeat': dict({ @@ -6402,21 +6401,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('51076966-2970-4b40-b6ba-d58c6a756dd7'), + '51076966-2970-4b40-b6ba-d58c6a756dd7', ]), 'text': 'Wochenendausflug planen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 16, 16, 756000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:16:16.756000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 0.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6431,7 +6430,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 19, 10, 919000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:19:10.919000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -6455,12 +6454,12 @@ }), 'history': list([ ]), - 'id': UUID('162f0bbe-a097-4a06-b4f4-8fbeed85d2ba'), + 'id': '162f0bbe-a097-4a06-b4f4-8fbeed85d2ba', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Lebensmittel und Haushaltsbedarf für die Woche einkaufen.', - 'priority': , + 'priority': 1.5, 'reminders': list([ ]), 'repeat': dict({ @@ -6475,21 +6474,21 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wocheneinkauf erledigen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 19, 15, 484000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:19:15.484000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), 'yesterDaily': None, }), dict({ - 'Type': , 'alias': None, - 'attribute': , + 'attribute': 'str', 'byHabitica': False, 'challenge': dict({ 'broken': None, @@ -6504,7 +6503,7 @@ 'completed': True, 'counterDown': None, 'counterUp': None, - 'createdAt': datetime.datetime(2024, 9, 21, 22, 18, 30, 646000, tzinfo=datetime.timezone.utc), + 'createdAt': '2024-09-21T22:18:30.646000+00:00', 'date': None, 'daysOfMonth': list([ ]), @@ -6528,12 +6527,12 @@ }), 'history': list([ ]), - 'id': UUID('3fa06743-aa0f-472b-af1a-f27c755e329c'), + 'id': '3fa06743-aa0f-472b-af1a-f27c755e329c', 'isDue': None, 'nextDue': list([ ]), 'notes': 'Wohnzimmer und Küche gründlich aufräumen.', - 'priority': , + 'priority': 2, 'reminders': list([ ]), 'repeat': dict({ @@ -6548,12 +6547,13 @@ 'startDate': None, 'streak': None, 'tags': list([ - UUID('64235347-55d0-4ba1-a86a-3428dcfdf319'), + '64235347-55d0-4ba1-a86a-3428dcfdf319', ]), 'text': 'Wohnung aufräumen', + 'type': 'todo', 'up': None, - 'updatedAt': datetime.datetime(2024, 9, 21, 22, 18, 34, 663000, tzinfo=datetime.timezone.utc), - 'userId': UUID('5f359083-ef78-4af0-985a-0b2c6d05797c'), + 'updatedAt': '2024-09-21T22:18:34.663000+00:00', + 'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c', 'value': 1.0, 'weeksOfMonth': list([ ]), 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/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index c015a881343..343d648e543 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @@ -77,3 +78,18 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_prevent_multiple_config_entries( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test we only allow one config entry.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "home_connect", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py index 145087073af..3696ea66c03 100644 --- a/tests/components/homeassistant_hardware/test_config_flow.py +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -23,6 +23,7 @@ from homeassistant.components.homeassistant_hardware.util import ( from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -106,7 +107,7 @@ class FakeFirmwareOptionsFlowHandler(BaseFirmwareOptionsFlow): @pytest.fixture(autouse=True) -def mock_test_firmware_platform( +async def mock_test_firmware_platform( hass: HomeAssistant, ) -> Generator[None]: """Fixture for a test config flow.""" @@ -116,6 +117,8 @@ def mock_test_firmware_platform( mock_integration(hass, mock_module) mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + await async_setup_component(hass, "homeassistant_hardware", {}) + with mock_config_flow(TEST_DOMAIN, FakeFirmwareConfigFlow): yield @@ -189,6 +192,10 @@ def mock_addon_info( "homeassistant.components.homeassistant_hardware.firmware_config_flow.get_otbr_addon_manager", return_value=mock_otbr_manager, ), + patch( + "homeassistant.components.homeassistant_hardware.util.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), patch( "homeassistant.components.homeassistant_hardware.firmware_config_flow.get_zigbee_flasher_addon_manager", return_value=mock_flasher_manager, @@ -197,6 +204,10 @@ def mock_addon_info( "homeassistant.components.homeassistant_hardware.firmware_config_flow.is_hassio", return_value=is_hassio, ), + patch( + "homeassistant.components.homeassistant_hardware.util.is_hassio", + return_value=is_hassio, + ), patch( "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type", return_value=app_type, diff --git a/tests/components/homeassistant_hardware/test_config_flow_failures.py b/tests/components/homeassistant_hardware/test_config_flow_failures.py index f5375fb51dd..c240d0198ca 100644 --- a/tests/components/homeassistant_hardware/test_config_flow_failures.py +++ b/tests/components/homeassistant_hardware/test_config_flow_failures.py @@ -1,6 +1,6 @@ """Test the Home Assistant hardware firmware config flow failure cases.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch import pytest @@ -9,7 +9,11 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import STEP_PICK_FIRMWARE_THREAD, STEP_PICK_FIRMWARE_ZIGBEE, ) -from homeassistant.components.homeassistant_hardware.util import ApplicationType +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningIntegration, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -548,21 +552,28 @@ async def test_options_flow_zigbee_to_thread_zha_configured( assert await hass.config_entries.async_setup(config_entry.entry_id) - # Set up ZHA as well - zha_config_entry = MockConfigEntry( - domain="zha", - data={"device": {"path": TEST_DEVICE}}, - ) - zha_config_entry.add_to_hass(hass) + # Pretend ZHA is using the stick + with patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.guess_hardware_owners", + return_value=[ + FirmwareInfo( + device=TEST_DEVICE, + firmware_type=ApplicationType.EZSP, + firmware_version="1.2.3.4", + source="zha", + owners=[OwningIntegration(config_entry_id="some_config_entry_id")], + ) + ], + ): + # Confirm options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) - # Confirm options flow - result = await hass.config_entries.options.async_init(config_entry.entry_id) + # Pick Thread + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) - # Pick Thread - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, - ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "zha_still_using_stick" diff --git a/tests/components/homeassistant_hardware/test_helpers.py b/tests/components/homeassistant_hardware/test_helpers.py new file mode 100644 index 00000000000..183995be7ce --- /dev/null +++ b/tests/components/homeassistant_hardware/test_helpers.py @@ -0,0 +1,185 @@ +"""Test hardware helpers.""" + +import logging +from unittest.mock import AsyncMock, MagicMock, Mock, call + +import pytest + +from homeassistant.components.homeassistant_hardware.const import DATA_COMPONENT +from homeassistant.components.homeassistant_hardware.helpers import ( + async_notify_firmware_info, + async_register_firmware_info_callback, + async_register_firmware_info_provider, +) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +FIRMWARE_INFO_EZSP = FirmwareInfo( + device="/dev/serial/by-id/device1", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="zha", + owners=[AsyncMock(is_running=AsyncMock(return_value=True))], +) + +FIRMWARE_INFO_SPINEL = FirmwareInfo( + device="/dev/serial/by-id/device2", + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[AsyncMock(is_running=AsyncMock(return_value=True))], +) + + +async def test_dispatcher_registration(hass: HomeAssistant) -> None: + """Test HardwareInfoDispatcher registration.""" + + await async_setup_component(hass, "homeassistant_hardware", {}) + + # Mock provider 1 with a synchronous method to pull firmware info + provider1_config_entry = MockConfigEntry( + domain="zha", + unique_id="some_unique_id1", + data={}, + ) + provider1_config_entry.add_to_hass(hass) + provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED) + + provider1_firmware = MagicMock(spec=["get_firmware_info"]) + provider1_firmware.get_firmware_info = MagicMock(return_value=FIRMWARE_INFO_EZSP) + async_register_firmware_info_provider(hass, "zha", provider1_firmware) + + # Mock provider 2 with an asynchronous method to pull firmware info + provider2_config_entry = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id2", + data={}, + ) + provider2_config_entry.add_to_hass(hass) + provider2_config_entry.mock_state(hass, ConfigEntryState.LOADED) + + provider2_firmware = MagicMock(spec=["async_get_firmware_info"]) + provider2_firmware.async_get_firmware_info = AsyncMock( + return_value=FIRMWARE_INFO_SPINEL + ) + async_register_firmware_info_provider(hass, "otbr", provider2_firmware) + + # Double registration won't work + with pytest.raises(ValueError, match="Domain zha is already registered"): + async_register_firmware_info_provider(hass, "zha", provider1_firmware) + + # We can iterate over the results + info = [i async for i in hass.data[DATA_COMPONENT].iter_firmware_info()] + assert info == [ + FIRMWARE_INFO_EZSP, + FIRMWARE_INFO_SPINEL, + ] + + callback1 = Mock() + cancel1 = async_register_firmware_info_callback( + hass, "/dev/serial/by-id/device1", callback1 + ) + + callback2 = Mock() + cancel2 = async_register_firmware_info_callback( + hass, "/dev/serial/by-id/device2", callback2 + ) + + # And receive notification callbacks + await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP) + await async_notify_firmware_info(hass, "otbr", firmware_info=FIRMWARE_INFO_SPINEL) + await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP) + cancel1() + await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP) + await async_notify_firmware_info(hass, "otbr", firmware_info=FIRMWARE_INFO_SPINEL) + cancel2() + + assert callback1.mock_calls == [ + call(FIRMWARE_INFO_EZSP), + call(FIRMWARE_INFO_EZSP), + ] + + assert callback2.mock_calls == [ + call(FIRMWARE_INFO_SPINEL), + call(FIRMWARE_INFO_SPINEL), + ] + + +async def test_dispatcher_iter_error_handling( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test HardwareInfoDispatcher ignoring errors from firmware info providers.""" + + await async_setup_component(hass, "homeassistant_hardware", {}) + + provider1_config_entry = MockConfigEntry( + domain="zha", + unique_id="some_unique_id1", + data={}, + ) + provider1_config_entry.add_to_hass(hass) + provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED) + + provider1_firmware = MagicMock(spec=["get_firmware_info"]) + provider1_firmware.get_firmware_info = MagicMock(side_effect=Exception("Boom!")) + async_register_firmware_info_provider(hass, "zha", provider1_firmware) + + provider2_config_entry = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id2", + data={}, + ) + provider2_config_entry.add_to_hass(hass) + provider2_config_entry.mock_state(hass, ConfigEntryState.LOADED) + + provider2_firmware = MagicMock(spec=["async_get_firmware_info"]) + provider2_firmware.async_get_firmware_info = AsyncMock( + return_value=FIRMWARE_INFO_SPINEL + ) + async_register_firmware_info_provider(hass, "otbr", provider2_firmware) + + with caplog.at_level(logging.ERROR): + info = [i async for i in hass.data[DATA_COMPONENT].iter_firmware_info()] + + assert info == [FIRMWARE_INFO_SPINEL] + assert "Error while getting firmware info from" in caplog.text + + +async def test_dispatcher_callback_error_handling( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test HardwareInfoDispatcher ignoring errors from firmware info callbacks.""" + + await async_setup_component(hass, "homeassistant_hardware", {}) + provider1_config_entry = MockConfigEntry( + domain="zha", + unique_id="some_unique_id1", + data={}, + ) + provider1_config_entry.add_to_hass(hass) + provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED) + + provider1_firmware = MagicMock(spec=["get_firmware_info"]) + provider1_firmware.get_firmware_info = MagicMock(return_value=FIRMWARE_INFO_EZSP) + async_register_firmware_info_provider(hass, "zha", provider1_firmware) + + callback1 = Mock(side_effect=Exception("Some error")) + async_register_firmware_info_callback(hass, "/dev/serial/by-id/device1", callback1) + + callback2 = Mock() + async_register_firmware_info_callback(hass, "/dev/serial/by-id/device1", callback2) + + with caplog.at_level(logging.ERROR): + await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP) + + assert "Error while notifying firmware info listener" in caplog.text + + assert callback1.mock_calls == [call(FIRMWARE_INFO_EZSP)] + assert callback2.mock_calls == [call(FIRMWARE_INFO_EZSP)] diff --git a/tests/components/homeassistant_hardware/test_util.py b/tests/components/homeassistant_hardware/test_util.py index 3f019a0409c..047de3e452c 100644 --- a/tests/components/homeassistant_hardware/test_util.py +++ b/tests/components/homeassistant_hardware/test_util.py @@ -1,18 +1,21 @@ """Test hardware utilities.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.components.hassio import AddonError, AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.helpers import ( + async_register_firmware_info_provider, +) from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, - FirmwareGuess, - FlasherApplicationType, - get_zha_device_path, - guess_firmware_type, - probe_silabs_firmware_type, + FirmwareInfo, + OwningAddon, + OwningIntegration, + guess_firmware_info, ) -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -21,7 +24,21 @@ ZHA_CONFIG_ENTRY = MockConfigEntry( unique_id="some_unique_id", data={ "device": { - "path": "socket://1.2.3.4:5678", + "path": "/dev/ttyUSB1", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, +) + +ZHA_CONFIG_ENTRY2 = MockConfigEntry( + domain="zha", + unique_id="some_other_unique_id", + data={ + "device": { + "path": "/dev/ttyUSB2", "baudrate": 115200, "flow_control": None, }, @@ -31,153 +48,202 @@ ZHA_CONFIG_ENTRY = MockConfigEntry( ) -def test_get_zha_device_path() -> None: - """Test extracting the ZHA device path from its config entry.""" - assert ( - get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"] - ) - - -def test_get_zha_device_path_ignored_discovery() -> None: - """Test extracting the ZHA device path from an ignored ZHA discovery.""" - config_entry = MockConfigEntry( - domain="zha", - unique_id="some_unique_id", - data={}, - version=4, - ) - - assert get_zha_device_path(config_entry) is None - - -async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: +async def test_guess_firmware_info_unknown(hass: HomeAssistant) -> None: """Test guessing the firmware type.""" - assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + await async_setup_component(hass, "homeassistant_hardware", {}) + + assert (await guess_firmware_info(hass, "/dev/missing")) == FirmwareInfo( + device="/dev/missing", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="unknown", + owners=[], ) -async def test_guess_firmware_type(hass: HomeAssistant) -> None: - """Test guessing the firmware.""" - path = ZHA_CONFIG_ENTRY.data["device"]["path"] +async def test_guess_firmware_info_integrations(hass: HomeAssistant) -> None: + """Test guessing the firmware via OTBR and ZHA.""" - ZHA_CONFIG_ENTRY.add_to_hass(hass) + await async_setup_component(hass, "homeassistant_hardware", {}) - ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="zha" + # One instance of ZHA and two OTBRs + zha = MockConfigEntry(domain="zha", unique_id="some_unique_id_1") + zha.add_to_hass(hass) + + otbr1 = MockConfigEntry(domain="otbr", unique_id="some_unique_id_2") + otbr1.add_to_hass(hass) + + otbr2 = MockConfigEntry(domain="otbr", unique_id="some_unique_id_3") + otbr2.add_to_hass(hass) + + # First ZHA is running with the stick + zha_firmware_info = FirmwareInfo( + device="/dev/serial/by-id/device1", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="zha", + owners=[AsyncMock(is_running=AsyncMock(return_value=True))], ) - # When ZHA is running, we indicate as such when guessing - ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + # First OTBR: neither the addon or the integration are loaded + otbr_firmware_info1 = FirmwareInfo( + device="/dev/serial/by-id/device1", + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + AsyncMock(is_running=AsyncMock(return_value=False)), + AsyncMock(is_running=AsyncMock(return_value=False)), + ], ) - mock_otbr_addon_manager = AsyncMock() - mock_multipan_addon_manager = AsyncMock() + # Second OTBR: fully running but is with an unrelated device + otbr_firmware_info2 = FirmwareInfo( + device="/dev/serial/by-id/device2", # An unrelated device + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + AsyncMock(is_running=AsyncMock(return_value=True)), + AsyncMock(is_running=AsyncMock(return_value=True)), + ], + ) - with ( - patch( - "homeassistant.components.homeassistant_hardware.util.is_hassio", - return_value=True, - ), - patch( - "homeassistant.components.homeassistant_hardware.util.get_otbr_addon_manager", - return_value=mock_otbr_addon_manager, - ), - patch( - "homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager", - return_value=mock_multipan_addon_manager, - ), - ): - mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError() - mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError() + mock_zha_hardware_info = MagicMock(spec=["get_firmware_info"]) + mock_zha_hardware_info.get_firmware_info = MagicMock(return_value=zha_firmware_info) + async_register_firmware_info_provider(hass, "zha", mock_zha_hardware_info) - # Hassio errors are ignored and we still go with ZHA - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) + async def mock_otbr_async_get_firmware_info( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> FirmwareInfo | None: + return { + otbr1.entry_id: otbr_firmware_info1, + otbr2.entry_id: otbr_firmware_info2, + }.get(config_entry.entry_id) - mock_otbr_addon_manager.async_get_addon_info.side_effect = None - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": "/some/other/device"}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) + mock_otbr_hardware_info = MagicMock(spec=["async_get_firmware_info"]) + mock_otbr_hardware_info.async_get_firmware_info = AsyncMock( + side_effect=mock_otbr_async_get_firmware_info + ) + async_register_firmware_info_provider(hass, "otbr", mock_otbr_hardware_info) - # We will prefer ZHA, as it is running (and actually pointing to the device) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) + # ZHA wins for the first stick, since it's actually running + assert ( + await guess_firmware_info(hass, "/dev/serial/by-id/device1") + ) == zha_firmware_info - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.NOT_RUNNING, - update_available=False, - version="1.0.0", - ) + # Second stick is communicating exclusively with the second OTBR + assert ( + await guess_firmware_info(hass, "/dev/serial/by-id/device2") + ) == otbr_firmware_info2 - # We will still prefer ZHA, as it is the one actually running - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) - - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) - - # Finally, ZHA loses out to OTBR - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr" - ) - - mock_multipan_addon_manager.async_get_addon_info.side_effect = None - mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) - - # Which will lose out to multi-PAN - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol" - ) + # If we stop ZHA, OTBR will take priority + zha_firmware_info.owners[0].is_running.return_value = False + otbr_firmware_info1.owners[0].is_running.return_value = True + assert ( + await guess_firmware_info(hass, "/dev/serial/by-id/device1") + ) == otbr_firmware_info1 -async def test_probe_silabs_firmware_type() -> None: - """Test probing Silabs firmware type.""" +async def test_owning_addon(hass: HomeAssistant) -> None: + """Test `OwningAddon`.""" + owning_addon = OwningAddon(slug="some-addon-slug") + + # Explicitly running with patch( - "homeassistant.components.homeassistant_hardware.util.Flasher.probe_app_type", - side_effect=RuntimeError, - ): - assert (await probe_silabs_firmware_type("/dev/ttyUSB0")) is None - - with patch( - "homeassistant.components.homeassistant_hardware.util.Flasher.probe_app_type", - side_effect=lambda self: setattr(self, "app_type", FlasherApplicationType.EZSP), - autospec=True, - ) as mock_probe_app_type: - # The application type constant is converted back and forth transparently - result = await probe_silabs_firmware_type( - "/dev/ttyUSB0", probe_methods=[ApplicationType.EZSP] + "homeassistant.components.homeassistant_hardware.util.WaitingAddonManager" + ) as mock_manager: + mock_manager.return_value.async_get_addon_info = AsyncMock( + return_value=AddonInfo( + available=True, + hostname="core_some_addon_slug", + options={}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) ) - assert result is ApplicationType.EZSP + assert (await owning_addon.is_running(hass)) is True - flasher = mock_probe_app_type.mock_calls[0].args[0] - assert flasher._probe_methods == [FlasherApplicationType.EZSP] + # Explicitly not running + with patch( + "homeassistant.components.homeassistant_hardware.util.WaitingAddonManager" + ) as mock_manager: + mock_manager.return_value.async_get_addon_info = AsyncMock( + return_value=AddonInfo( + available=True, + hostname="core_some_addon_slug", + options={}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + ) + assert (await owning_addon.is_running(hass)) is False + + # Failed to get status + with patch( + "homeassistant.components.homeassistant_hardware.util.WaitingAddonManager" + ) as mock_manager: + mock_manager.return_value.async_get_addon_info = AsyncMock( + side_effect=AddonError() + ) + assert (await owning_addon.is_running(hass)) is False + + +async def test_owning_integration(hass: HomeAssistant) -> None: + """Test `OwningIntegration`.""" + config_entry = MockConfigEntry(domain="mock_domain", unique_id="some_unique_id") + config_entry.add_to_hass(hass) + + owning_integration = OwningIntegration(config_entry_id=config_entry.entry_id) + + # Explicitly running + config_entry.mock_state(hass, ConfigEntryState.LOADED) + assert (await owning_integration.is_running(hass)) is True + + # Explicitly not running + config_entry.mock_state(hass, ConfigEntryState.NOT_LOADED) + assert (await owning_integration.is_running(hass)) is False + + # Missing config entry + owning_integration2 = OwningIntegration(config_entry_id="some_nonexistenct_id") + assert (await owning_integration2.is_running(hass)) is False + + +async def test_firmware_info(hass: HomeAssistant) -> None: + """Test `FirmwareInfo`.""" + + owner1 = AsyncMock() + owner2 = AsyncMock() + + firmware_info = FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version="1.0.0", + source="zha", + owners=[owner1, owner2], + ) + + # Both running + owner1.is_running.return_value = True + owner2.is_running.return_value = True + assert (await firmware_info.is_running(hass)) is True + + # Only one running + owner1.is_running.return_value = True + owner2.is_running.return_value = False + assert (await firmware_info.is_running(hass)) is False + + # No owners + firmware_info2 = FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version="1.0.0", + source="zha", + owners=[], + ) + + assert (await firmware_info2.is_running(hass)) is False diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 15eeb205537..8e90039a4fc 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, - FirmwareGuess, + FirmwareInfo, ) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import HomeAssistant @@ -32,11 +32,13 @@ async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( - "homeassistant.components.homeassistant_sky_connect.guess_firmware_type", - return_value=FirmwareGuess( - is_running=True, + "homeassistant.components.homeassistant_sky_connect.guess_firmware_info", + return_value=FirmwareInfo( + device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + firmware_version=None, firmware_type=ApplicationType.SPINEL, source="otbr", + owners=[], ), ): await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 5d534dad1e7..57d63c7441e 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -8,7 +8,7 @@ from homeassistant.components import zha from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, - FirmwareGuess, + FirmwareInfo, ) from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -49,11 +49,13 @@ async def test_setup_entry( return_value=onboarded, ), patch( - "homeassistant.components.homeassistant_yellow.guess_firmware_type", - return_value=FirmwareGuess( # Nothing is setup - is_running=False, + "homeassistant.components.homeassistant_yellow.guess_firmware_info", + return_value=FirmwareInfo( # Nothing is setup + device="/dev/ttyAMA1", + firmware_version=None, firmware_type=ApplicationType.EZSP, source="unknown", + owners=[], ), ), ): diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 692383b4794..91b1e30e4f8 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -782,6 +782,174 @@ 'state': '230.0', }) # --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_rssi:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Plug-In Battery', + 'model_id': 'HWE-BAT', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.00', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_rssi:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_rssi', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi RSSI', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_rssi', + 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_rssi', + 'unit_of_measurement': 'dB', + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_rssi:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi RSSI', + 'state_class': , + 'unit_of_measurement': 'dB', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-77', + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_ssid:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Plug-In Battery', + 'model_id': 'HWE-BAT', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.00', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_ssid:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi SSID', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_ssid', + 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'simulating v1 support', + }) +# --- # name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_apparent_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -14363,6 +14531,174 @@ 'state': '0.0', }) # --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_ssid:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Wi-Fi P1 Meter', + 'model_id': 'HWE-P1', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.19', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_ssid:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi SSID', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_ssid', + 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'My Wi-Fi', + }) +# --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_strength:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Wi-Fi P1 Meter', + 'model_id': 'HWE-P1', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.19', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_strength:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi strength', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_strength', + 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_wi_fi_strength:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi strength', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 94a59551eb4..fe709570239 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -108,6 +108,8 @@ pytestmark = [ "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_ssid", + "sensor.device_wi_fi_strength", ], ), ( @@ -304,6 +306,8 @@ pytestmark = [ "sensor.device_state_of_charge", "sensor.device_uptime", "sensor.device_voltage", + "sensor.device_wi_fi_rssi", + "sensor.device_wi_fi_ssid", ], ), ], @@ -453,6 +457,7 @@ async def test_sensors( "sensor.device_frequency", "sensor.device_uptime", "sensor.device_voltage", + "sensor.device_wi_fi_rssi", ], ), ], @@ -561,6 +566,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -610,6 +616,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -667,6 +674,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -718,6 +726,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -758,6 +767,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -809,6 +819,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -849,6 +860,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -897,6 +909,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_strength", ], ), ], diff --git a/tests/components/knx/README.md b/tests/components/knx/README.md index ef8398b3d17..71218010b45 100644 --- a/tests/components/knx/README.md +++ b/tests/components/knx/README.md @@ -3,17 +3,22 @@ A KNXTestKit instance can be requested from a fixture. It provides convenience methods to test outgoing KNX telegrams and inject incoming telegrams. To test something add a test function requesting the `hass` and `knx` fixture and -set up the KNX integration by passing a KNX config dict to `knx.setup_integration`. +set up the KNX integration with `knx.setup_integration`. +You can pass a KNX YAML-config dict or a ConfigStore fixture filename to the setup method. The fixture should be placed in the `tests/components/knx/fixtures` directory. ```python -async def test_something(hass, knx): - await knx.setup_integration({ +async def test_some_yaml(hass: HomeAssistant, knx: KNXTestKit): + await knx.setup_integration( + yaml_config={ "switch": { "name": "test_switch", "address": "1/2/3", } } ) + +async def test_some_config_store(hass: HomeAssistant, knx: KNXTestKit): + await knx.setup_integration(config_store_fixture="config_store_filename.json") ``` ## Asserting outgoing telegrams diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 4e50836bb79..c9092a1774f 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -44,7 +44,6 @@ from tests.common import MockConfigEntry, load_json_object_fixture from tests.typing import WebSocketGenerator FIXTURE_PROJECT_DATA = load_json_object_fixture("project.json", KNX_DOMAIN) -FIXTURE_CONFIG_STORAGE_DATA = load_json_object_fixture("config_store.json", KNX_DOMAIN) class KNXTestKit: @@ -52,10 +51,16 @@ class KNXTestKit: INDIVIDUAL_ADDRESS = "1.2.3" - def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: + def __init__( + self, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], + ) -> None: """Init KNX test helper class.""" self.hass: HomeAssistant = hass self.mock_config_entry: MockConfigEntry = mock_config_entry + self.hass_storage: dict[str, Any] = hass_storage self.xknx: XKNX # outgoing telegrams will be put in the List instead of sent to the interface # telegrams to an InternalGroupAddress won't be queued here @@ -69,7 +74,10 @@ class KNXTestKit: assert test_state.attributes.get(attribute) == value async def setup_integration( - self, config: ConfigType, add_entry_to_hass: bool = True + self, + yaml_config: ConfigType | None = None, + config_store_fixture: str | None = None, + add_entry_to_hass: bool = True, ) -> None: """Create the KNX integration.""" @@ -101,15 +109,21 @@ class KNXTestKit: self.xknx = args[0] return DEFAULT + if config_store_fixture: + self.hass_storage[KNX_CONFIG_STORAGE_KEY] = load_json_object_fixture( + config_store_fixture, KNX_DOMAIN + ) + if add_entry_to_hass: self.mock_config_entry.add_to_hass(self.hass) + knx_config = {KNX_DOMAIN: yaml_config or {}} with patch( "xknx.xknx.knx_interface_factory", return_value=knx_ip_interface_mock(), side_effect=fish_xknx, ): - await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) + await async_setup_component(self.hass, KNX_DOMAIN, knx_config) await self.hass.async_block_till_done() ######################## @@ -306,9 +320,13 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -async def knx(hass: HomeAssistant, mock_config_entry: MockConfigEntry): +async def knx( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +): """Create a KNX TestKit instance.""" - knx_test_kit = KNXTestKit(hass, mock_config_entry) + knx_test_kit = KNXTestKit(hass, mock_config_entry, hass_storage) yield knx_test_kit await knx_test_kit.assert_no_telegram() @@ -322,12 +340,6 @@ def load_knxproj(hass_storage: dict[str, Any]) -> None: } -@pytest.fixture -def load_config_store(hass_storage: dict[str, Any]) -> None: - """Mock KNX config store data.""" - hass_storage[KNX_CONFIG_STORAGE_KEY] = FIXTURE_CONFIG_STORAGE_DATA - - @pytest.fixture async def create_ui_entity( hass: HomeAssistant, diff --git a/tests/components/knx/fixtures/config_store_binarysensor.json b/tests/components/knx/fixtures/config_store_binarysensor.json new file mode 100644 index 00000000000..427867cff8c --- /dev/null +++ b/tests/components/knx/fixtures/config_store_binarysensor.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "minor_version": 1, + "key": "knx/config_store.json", + "data": { + "entities": { + "light": {}, + "binary_sensor": { + "knx_es_01JJP1XDQRXB0W6YYGXW6Y1X10": { + "entity": { + "name": "test", + "device_info": null, + "entity_category": null + }, + "knx": { + "ga_sensor": { + "state": "3/2/21", + "passive": [] + }, + "respond_to_read": false, + "sync_state": true + } + } + } + } + } +} diff --git a/tests/components/knx/fixtures/config_store.json b/tests/components/knx/fixtures/config_store_light_switch.json similarity index 100% rename from tests/components/knx/fixtures/config_store.json rename to tests/components/knx/fixtures/config_store_light_switch.json diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 4b58801a8a0..b93b7e965df 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -329,7 +329,7 @@ async def test_binary_sensor_ui_create( knx_data: dict[str, Any], ) -> None: """Test creating a binary sensor.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.BINARY_SENSOR, entity_data={"name": "test"}, @@ -340,3 +340,10 @@ async def test_binary_sensor_ui_create( await knx.receive_response("2/2/2", not knx_data.get("invert")) state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON + + +async def test_binary_sensor_ui_load(knx: KNXTestKit) -> None: + """Test loading a binary sensor from storage.""" + await knx.setup_integration(config_store_fixture="config_store_binarysensor.json") + await knx.assert_read("3/2/21", response=True, ignore_order=True) + knx.assert_state("binary_sensor.test", STATE_ON) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 8ed79f837bb..3e4c9408542 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1278,7 +1278,7 @@ async def test_options_flow_connection_type( # usage of the already running XKNX instance for gateway scanner gateway = _gateway_descriptor("192.168.0.1", 3675) - await knx.setup_integration({}) + await knx.setup_integration() menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) with patch( diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py index 116f4b5d839..aee0a4036ff 100644 --- a/tests/components/knx/test_config_store.py +++ b/tests/components/knx/test_config_store.py @@ -25,7 +25,7 @@ async def test_create_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_name = "Test no device" @@ -69,7 +69,7 @@ async def test_create_entity_error( hass_ws_client: WebSocketGenerator, ) -> None: """Test unsuccessful entity creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) # create entity with invalid platform @@ -116,7 +116,7 @@ async def test_update_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity update.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -163,7 +163,7 @@ async def test_update_entity_error( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity update.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -238,7 +238,7 @@ async def test_delete_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity deletion.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -270,7 +270,7 @@ async def test_delete_entity_error( hass_storage: dict[str, Any], ) -> None: """Test unsuccessful entity deletion.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) # delete unknown entity @@ -307,7 +307,7 @@ async def test_get_entity_config( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity config retrieval.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -355,7 +355,7 @@ async def test_get_entity_config_error( error_message_start: str, ) -> None: """Test entity config retrieval errors.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( @@ -376,7 +376,7 @@ async def test_validate_entity( hass_ws_client: WebSocketGenerator, ) -> None: """Test entity validation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( diff --git a/tests/components/knx/test_device.py b/tests/components/knx/test_device.py index 04ff02f0611..356640dd8d0 100644 --- a/tests/components/knx/test_device.py +++ b/tests/components/knx/test_device.py @@ -22,7 +22,7 @@ async def test_create_device( hass_ws_client: WebSocketGenerator, ) -> None: """Test device creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( @@ -50,12 +50,11 @@ async def test_remove_device( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - load_config_store: None, hass_storage: dict[str, Any], ) -> None: """Test device removal.""" assert await async_setup_component(hass, "config", {}) - await knx.setup_integration({}) + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") client = await hass_ws_client(hass) await knx.assert_read("1/0/21", response=True, ignore_order=True) # test light diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index e5f776a9404..e4a208906c6 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -28,7 +28,7 @@ async def test_if_fires_on_telegram( knx: KNXTestKit, ) -> None: """Test telegram device triggers firing.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -124,7 +124,7 @@ async def test_default_if_fires_on_telegram( # by default (without a user changing any) extra_fields are not added to the trigger and # pre 2024.2 device triggers did only support "destination" field so they didn't have # "group_value_write", "group_value_response", "group_value_read", "incoming", "outgoing" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -206,7 +206,7 @@ async def test_remove_device_trigger( ) -> None: """Test for removed callback when device trigger not used.""" automation_name = "telegram_trigger_automation" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -256,7 +256,7 @@ async def test_get_triggers( knx: KNXTestKit, ) -> None: """Test we get the expected device triggers from knx.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -279,7 +279,7 @@ async def test_get_trigger_capabilities( knx: KNXTestKit, ) -> None: """Test we get the expected capabilities telegram device trigger.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -361,7 +361,7 @@ async def test_invalid_device_trigger( caplog: pytest.LogCaptureFixture, ) -> None: """Test invalid telegram device trigger configuration.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -404,7 +404,7 @@ async def test_invalid_trigger_configuration( knx: KNXTestKit, ) -> None: """Test invalid telegram device trigger configuration at attach_trigger.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index bb60e66f7e7..6d4bf7e6007 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -1,5 +1,7 @@ """Tests for the diagnostics data provided by the KNX integration.""" +from typing import Any + import pytest from syrupy import SnapshotAssertion from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT @@ -40,7 +42,7 @@ async def test_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -60,7 +62,7 @@ async def test_diagnostic_config_error( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -76,6 +78,7 @@ async def test_diagnostic_config_error( async def test_diagnostic_redact( hass: HomeAssistant, hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test diagnostics redacting data.""" @@ -95,8 +98,8 @@ async def test_diagnostic_redact( CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", }, ) - knx: KNXTestKit = KNXTestKit(hass, mock_config_entry) - await knx.setup_integration({}) + knx: KNXTestKit = KNXTestKit(hass, mock_config_entry, hass_storage) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -117,7 +120,7 @@ async def test_diagnostics_project( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() knx.xknx.version = "0.0.0" # snapshot will contain project specific fields in `project_info` assert ( diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 75cd5d1eb21..579f9b143a2 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -226,7 +226,7 @@ async def test_init_connection_handling( data=config_entry_data, ) knx.mock_config_entry = config_entry - await knx.setup_integration({}) + await knx.setup_integration() assert hass.data.get(KNX_DOMAIN) is not None @@ -280,7 +280,7 @@ async def _init_switch_and_wait_for_first_state_updater_run( title="KNX", domain=KNX_DOMAIN, data=config_entry_data ) knx.mock_config_entry = config_entry - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.SWITCH, knx_data={ @@ -354,7 +354,7 @@ async def test_async_remove_entry( }, ) knx.mock_config_entry = config_entry - await knx.setup_integration({}) + await knx.setup_integration() with ( patch("pathlib.Path.unlink") as unlink_mock, diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 79114d4ffd5..4de366c69f0 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -25,7 +25,7 @@ async def test_diagnostic_entities( freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostic entities.""" - await knx.setup_integration({}) + await knx.setup_integration() for entity_id in ( "sensor.knx_interface_individual_address", @@ -103,7 +103,7 @@ async def test_removed_entity( with patch( "xknx.core.connection_manager.ConnectionManager.unregister_connection_state_changed_cb" ) as unregister_mock: - await knx.setup_integration({}) + await knx.setup_integration() entity_registry.async_update_entity( "sensor.knx_interface_connection_established", @@ -120,7 +120,7 @@ async def test_remove_interface_device( ) -> None: """Test device removal.""" assert await async_setup_component(hass, "config", {}) - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) knx_devices = device_registry.devices.get_devices_for_config_entry_id( knx.mock_config_entry.entry_id diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 6ba6090d60d..fb0246763a4 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -1176,7 +1176,7 @@ async def test_light_ui_create( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a light.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1213,7 +1213,7 @@ async def test_light_ui_color_temp( raw_ct: tuple[int, ...], ) -> None: """Test creating a color-temp light.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1250,7 +1250,7 @@ async def test_light_ui_multi_mode( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a light with multiple color modes.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1335,13 +1335,11 @@ async def test_light_ui_multi_mode( async def test_light_ui_load( - hass: HomeAssistant, knx: KNXTestKit, - load_config_store: None, entity_registry: er.EntityRegistry, ) -> None: """Test loading a light from storage.""" - await knx.setup_integration({}) + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") await knx.assert_read("1/0/21", response=True, ignore_order=True) # unrelated switch in config store diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index f70389dbc92..c4b48b5e81d 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -111,7 +111,7 @@ async def test_send( expected_apci, ) -> None: """Test `knx.send` service.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.services.async_call( "knx", @@ -127,7 +127,7 @@ async def test_send( async def test_read(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test `knx.read` service.""" - await knx.setup_integration({}) + await knx.setup_integration() # send read telegram await hass.services.async_call("knx", "read", {"address": "1/1/1"}, blocking=True) @@ -150,7 +150,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: events = async_capture_events(hass, "knx_event") test_address = "1/2/3" - await knx.setup_integration({}) + await knx.setup_integration() # no event registered await knx.receive_write(test_address, True) @@ -200,7 +200,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: test_entity = "fake.entity" test_attribute = "fake_attribute" - await knx.setup_integration({}) + await knx.setup_integration() # no exposure registered hass.states.async_set(test_entity, STATE_ON, {}) @@ -265,7 +265,7 @@ async def test_reload_service( knx: KNXTestKit, ) -> None: """Test reload service.""" - await knx.setup_integration({}) + await knx.setup_integration() with ( patch( @@ -285,7 +285,7 @@ async def test_reload_service( async def test_service_setup_failed(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test service setup failed.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.config_entries.async_unload(knx.mock_config_entry.entry_id) with pytest.raises(HomeAssistantError) as exc_info: diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index bc0a6b27675..969c11b8e1a 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -155,7 +155,7 @@ async def test_switch_ui_create( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a switch.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.SWITCH, entity_data={"name": "test"}, @@ -171,3 +171,16 @@ async def test_switch_ui_create( await knx.receive_response("2/2/2", True) state = hass.states.get("switch.test") assert state.state is STATE_ON + + +async def test_switch_ui_load(knx: KNXTestKit) -> None: + """Test loading a switch from storage.""" + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") + + await knx.assert_read("1/0/45", response=True, ignore_order=True) + # unrelated light in config store + await knx.assert_read("1/0/21", response=True, ignore_order=True) + knx.assert_state( + "switch.none_test", # has_entity_name with unregistered device -> none_test + STATE_ON, + ) diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index 883e8ccbb2d..840959bb6c5 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -70,7 +70,7 @@ async def test_store_telegam_history( hass_storage: dict[str, Any], ) -> None: """Test storing telegram history.""" - await knx.setup_integration({}) + await knx.setup_integration() await knx.receive_write("1/3/4", True) await hass.services.async_call( @@ -94,7 +94,7 @@ async def test_load_telegam_history( ) -> None: """Test telegram history restoration.""" hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} - await knx.setup_integration({}) + await knx.setup_integration() loaded_telegrams = hass.data[KNX_MODULE_KEY].telegrams.recent_telegrams assert assert_telegram_history(loaded_telegrams) # TelegramDict "payload" is a tuple, this shall be restored when loading from JSON @@ -113,7 +113,7 @@ async def test_remove_telegam_history( knx.mock_config_entry, data=knx.mock_config_entry.data | {CONF_KNX_TELEGRAM_LOG_SIZE: 0}, ) - await knx.setup_integration({}, add_entry_to_hass=False) + await knx.setup_integration(add_entry_to_hass=False) # Store.async_remove() is mocked by hass_storage - check that data was removed. assert "knx/telegrams_history.json" not in hass_storage assert not hass.data[KNX_MODULE_KEY].telegrams.recent_telegrams diff --git a/tests/components/knx/test_trigger.py b/tests/components/knx/test_trigger.py index 73e8b10840e..1ce42a23482 100644 --- a/tests/components/knx/test_trigger.py +++ b/tests/components/knx/test_trigger.py @@ -18,7 +18,7 @@ async def test_telegram_trigger( knx: KNXTestKit, ) -> None: """Test telegram triggers firing.""" - await knx.setup_integration({}) + await knx.setup_integration() # "id" field added to action to test if `trigger_data` passed correctly in `async_attach_trigger` assert await async_setup_component( @@ -105,7 +105,7 @@ async def test_telegram_trigger_dpt_option( expected_unit: str | None, ) -> None: """Test telegram trigger type option.""" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, automation.DOMAIN, @@ -190,7 +190,7 @@ async def test_telegram_trigger_options( direction_options: dict[str, bool], ) -> None: """Test telegram trigger options.""" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, automation.DOMAIN, @@ -266,7 +266,7 @@ async def test_remove_telegram_trigger( ) -> None: """Test for removed callback when telegram trigger not used.""" automation_name = "telegram_trigger_automation" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, @@ -311,7 +311,7 @@ async def test_invalid_trigger( caplog: pytest.LogCaptureFixture, ) -> None: """Test invalid telegram trigger configuration.""" - await knx.setup_integration({}) + await knx.setup_integration() caplog.clear() with caplog.at_level(logging.ERROR): assert await async_setup_component( diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index a34f126e4f4..7054d415ee9 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -20,7 +20,7 @@ async def test_knx_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/info command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/info"}) @@ -39,7 +39,7 @@ async def test_knx_info_command_with_project( load_knxproj: None, ) -> None: """Test knx/info command with loaded project.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/info"}) @@ -65,7 +65,7 @@ async def test_knx_project_file_process( _password = "pw-test" _parse_result = FIXTURE_PROJECT_DATA - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert not hass.data[KNX_MODULE_KEY].project.loaded @@ -100,7 +100,7 @@ async def test_knx_project_file_process_error( hass_ws_client: WebSocketGenerator, ) -> None: """Test knx/project_file_process exception handling.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert not hass.data[KNX_MODULE_KEY].project.loaded @@ -134,7 +134,7 @@ async def test_knx_project_file_remove( hass_storage: dict[str, Any], ) -> None: """Test knx/project_file_remove command.""" - await knx.setup_integration({}) + await knx.setup_integration() assert hass_storage[KNX_PROJECT_STORAGE_KEY] client = await hass_ws_client(hass) assert hass.data[KNX_MODULE_KEY].project.loaded @@ -154,7 +154,7 @@ async def test_knx_get_project( load_knxproj: None, ) -> None: """Test retrieval of kxnproject from store.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert hass.data[KNX_MODULE_KEY].project.loaded @@ -169,7 +169,7 @@ async def test_knx_group_monitor_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/group_monitor_info command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/group_monitor_info"}) @@ -184,7 +184,7 @@ async def test_knx_group_telegrams_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/group_telegrams command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "knx/group_telegrams"}) @@ -338,7 +338,7 @@ async def test_knx_subscribe_telegrams_command_project( load_knxproj: None, ) -> None: """Test knx/subscribe_telegrams command with project data.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/subscribe_telegrams"}) res = await client.receive_json() @@ -405,7 +405,7 @@ async def test_websocket_when_config_entry_unloaded( endpoint: str, ) -> None: """Test websocket connection when config entry is unloaded.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.config_entries.async_unload(knx.mock_config_entry.entry_id) client = await hass_ws_client(hass) 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/letpot/__init__.py b/tests/components/letpot/__init__.py index ac552f907d4..d4570ce44be 100644 --- a/tests/components/letpot/__init__.py +++ b/tests/components/letpot/__init__.py @@ -2,7 +2,12 @@ import datetime -from letpot.models import AuthenticationInfo, LetPotDeviceErrors, LetPotDeviceStatus +from letpot.models import ( + AuthenticationInfo, + LetPotDeviceErrors, + LetPotDeviceStatus, + TemperatureUnit, +) from homeassistant.core import HomeAssistant @@ -26,17 +31,21 @@ AUTHENTICATION = AuthenticationInfo( ) STATUS = LetPotDeviceStatus( - errors=LetPotDeviceErrors(low_water=False), + errors=LetPotDeviceErrors(low_water=True, low_nutrients=False, refill_error=False), light_brightness=500, light_mode=1, - light_schedule_end=datetime.time(12, 10), - light_schedule_start=datetime.time(12, 0), + light_schedule_end=datetime.time(18, 0), + light_schedule_start=datetime.time(8, 0), online=True, plant_days=1, pump_mode=1, pump_nutrient=None, pump_status=0, - raw=[77, 0, 1, 18, 98, 1, 0, 0, 1, 1, 1, 0, 1, 12, 0, 12, 10, 1, 244, 0, 0, 0], + raw=[], # Not used by integration, and it requires a real device to get system_on=True, system_sound=False, + temperature_unit=TemperatureUnit.CELSIUS, + temperature_value=18, + water_mode=1, + water_level=100, ) diff --git a/tests/components/letpot/conftest.py b/tests/components/letpot/conftest.py index 3e948ad0ac2..454d4e235db 100644 --- a/tests/components/letpot/conftest.py +++ b/tests/components/letpot/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Generator from unittest.mock import AsyncMock, patch -from letpot.models import LetPotDevice +from letpot.models import DeviceFeature, LetPotDevice import pytest from homeassistant.components.letpot.const import ( @@ -47,9 +47,9 @@ def mock_client() -> Generator[AsyncMock]: client.refresh_token.return_value = AUTHENTICATION client.get_devices.return_value = [ LetPotDevice( - serial_number="LPH21ABCD", + serial_number="LPH63ABCD", name="Garden", - device_type="LPH21", + device_type="LPH63", is_online=True, is_remote=False, ) @@ -65,8 +65,16 @@ def mock_device_client() -> Generator[AsyncMock]: autospec=True, ) as mock_device_client: device_client = mock_device_client.return_value - device_client.device_model_code = "LPH21" - device_client.device_model_name = "LetPot Air" + device_client.device_features = ( + DeviceFeature.LIGHT_BRIGHTNESS_LEVELS + | DeviceFeature.NUTRIENT_BUTTON + | DeviceFeature.PUMP_AUTO + | DeviceFeature.PUMP_STATUS + | DeviceFeature.TEMPERATURE + | DeviceFeature.WATER_LEVEL + ) + device_client.device_model_code = "LPH63" + device_client.device_model_name = "LetPot Max" subscribe_callbacks: list[Callable] = [] diff --git a/tests/components/letpot/snapshots/test_switch.ambr b/tests/components/letpot/snapshots/test_switch.ambr new file mode 100644 index 00000000000..28ca9603760 --- /dev/null +++ b/tests/components/letpot/snapshots/test_switch.ambr @@ -0,0 +1,185 @@ +# serializer version: 1 +# name: test_all_entities[switch.garden_alarm_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_alarm_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm sound', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_sound', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_alarm_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_alarm_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Alarm sound', + }), + 'context': , + 'entity_id': 'switch.garden_alarm_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[switch.garden_auto_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_auto_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto mode', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_auto_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_auto_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Auto mode', + }), + 'context': , + 'entity_id': 'switch.garden_auto_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[switch.garden_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Power', + }), + 'context': , + 'entity_id': 'switch.garden_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[switch.garden_pump_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_pump_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pump cycling', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_cycling', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_pump_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_pump_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Pump cycling', + }), + 'context': , + 'entity_id': 'switch.garden_pump_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/letpot/snapshots/test_time.ambr b/tests/components/letpot/snapshots/test_time.ambr new file mode 100644 index 00000000000..66f6648c202 --- /dev/null +++ b/tests/components/letpot/snapshots/test_time.ambr @@ -0,0 +1,93 @@ +# serializer version: 1 +# name: test_all_entities[time.garden_light_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': , + 'entity_id': 'time.garden_light_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light off', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_schedule_end', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[time.garden_light_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light off', + }), + 'context': , + 'entity_id': 'time.garden_light_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18:00:00', + }) +# --- +# name: test_all_entities[time.garden_light_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': , + 'entity_id': 'time.garden_light_on', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light on', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_schedule_start', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[time.garden_light_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light on', + }), + 'context': , + 'entity_id': 'time.garden_light_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '08:00:00', + }) +# --- diff --git a/tests/components/letpot/test_switch.py b/tests/components/letpot/test_switch.py index d51721c3348..b166d551adb 100644 --- a/tests/components/letpot/test_switch.py +++ b/tests/components/letpot/test_switch.py @@ -1,17 +1,35 @@ """Test switch entities for the LetPot integration.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from letpot.exceptions import LetPotConnectionException, LetPotException import pytest +from syrupy import SnapshotAssertion from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/letpot/test_time.py b/tests/components/letpot/test_time.py index 44a03e565c0..82e69979067 100644 --- a/tests/components/letpot/test_time.py +++ b/tests/components/letpot/test_time.py @@ -1,18 +1,36 @@ """Test time entities for the LetPot integration.""" from datetime import time -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from letpot.exceptions import LetPotConnectionException, LetPotException import pytest +from syrupy import SnapshotAssertion from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test time entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.TIME]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index 640264eb207..8f1e85f28ff 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -5,8 +5,10 @@ from unittest.mock import AsyncMock from linear_garage_door import InvalidLoginError import pytest +from homeassistant.components.linear_garage_door.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from . import setup_integration @@ -51,3 +53,23 @@ async def test_setup_failure( await setup_integration(hass, mock_config_entry, []) assert mock_config_entry.state == entry_state + + +async def test_repair_issue( + hass: HomeAssistant, + mock_linear: AsyncMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test reauth trigger setup.""" + + await setup_integration(hass, mock_config_entry, []) + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index d05c340dac2..b2dd3d048ec 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -391,6 +391,25 @@ async def test_service_call_with_ascii_qos_retain_flags( blocking=True, ) assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] == "" + assert mqtt_mock.async_publish.call_args[0][2] == 2 + assert not mqtt_mock.async_publish.call_args[0][3] + + mqtt_mock.reset_mock() + + # Test service call without payload + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_QOS: "2", + mqtt.ATTR_RETAIN: "no", + }, + blocking=True, + ) + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] is None assert mqtt_mock.async_publish.call_args[0][2] == 2 assert not mqtt_mock.async_publish.call_args[0][3] 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/overseerr/fixtures/webhook_config.json b/tests/components/overseerr/fixtures/webhook_config.json index 40028e1f80f..2b3388444d2 100644 --- a/tests/components/overseerr/fixtures/webhook_config.json +++ b/tests/components/overseerr/fixtures/webhook_config.json @@ -2,7 +2,7 @@ "enabled": true, "types": 222, "options": { - "jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdb_idd\":\"{{media_tmdbid}}\",\"tvdb_id\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requested_by_email\":\"{{requestedBy_email}}\",\"requested_by_username\":\"{{requestedBy_username}}\",\"requested_by_avatar\":\"{{requestedBy_avatar}}\",\"requested_by_settings_discord_id\":\"{{requestedBy_settings_discordId}}\",\"requested_by_settings_telegram_chat_id\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reported_by_email\":\"{{reportedBy_email}}\",\"reported_by_username\":\"{{reportedBy_username}}\",\"reported_by_avatar\":\"{{reportedBy_avatar}}\",\"reported_by_settings_discord_id\":\"{{reportedBy_settings_discordId}}\",\"reported_by_settings_telegram_chat_id\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commented_by_email\":\"{{commentedBy_email}}\",\"commented_by_username\":\"{{commentedBy_username}}\",\"commented_by_avatar\":\"{{commentedBy_avatar}}\",\"commented_by_settings_discord_id\":\"{{commentedBy_settings_discordId}}\",\"commented_by_settings_telegram_chat_id\":\"{{commentedBy_settings_telegramChatId}}\"}}", + "jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdb_id\":\"{{media_tmdbid}}\",\"tvdb_id\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requested_by_email\":\"{{requestedBy_email}}\",\"requested_by_username\":\"{{requestedBy_username}}\",\"requested_by_avatar\":\"{{requestedBy_avatar}}\",\"requested_by_settings_discord_id\":\"{{requestedBy_settings_discordId}}\",\"requested_by_settings_telegram_chat_id\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reported_by_email\":\"{{reportedBy_email}}\",\"reported_by_username\":\"{{reportedBy_username}}\",\"reported_by_avatar\":\"{{reportedBy_avatar}}\",\"reported_by_settings_discord_id\":\"{{reportedBy_settings_discordId}}\",\"reported_by_settings_telegram_chat_id\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commented_by_email\":\"{{commentedBy_email}}\",\"commented_by_username\":\"{{commentedBy_username}}\",\"commented_by_avatar\":\"{{commentedBy_avatar}}\",\"commented_by_settings_discord_id\":\"{{commentedBy_settings_discordId}}\",\"commented_by_settings_telegram_chat_id\":\"{{commentedBy_settings_telegramChatId}}\"}}", "webhookUrl": "http://10.10.10.10:8123/api/webhook/test-webhook-id" } } diff --git a/tests/components/peblar/snapshots/test_switch.ambr b/tests/components/peblar/snapshots/test_switch.ambr index 53829278593..426b48b6838 100644 --- a/tests/components/peblar/snapshots/test_switch.ambr +++ b/tests/components/peblar/snapshots/test_switch.ambr @@ -1,4 +1,50 @@ # serializer version: 1 +# name: test_entities[switch][switch.peblar_ev_charger_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.peblar_ev_charger_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge', + 'unique_id': '23-45-A4O-MOF_charge', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch][switch.peblar_ev_charger_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Peblar EV Charger Charge', + }), + 'context': , + 'entity_id': 'switch.peblar_ev_charger_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_entities[switch][switch.peblar_ev_charger_force_single_phase-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/peblar/test_number.py b/tests/components/peblar/test_number.py index 57469fecbc6..fa49b6ab116 100644 --- a/tests/components/peblar/test_number.py +++ b/tests/components/peblar/test_number.py @@ -14,18 +14,19 @@ from homeassistant.components.number import ( from homeassistant.components.peblar.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import MockConfigEntry, snapshot_platform - -pytestmark = [ - pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True), - pytest.mark.usefixtures("init_integration"), -] +from tests.common import ( + MockConfigEntry, + mock_restore_cache_with_extra_data, + snapshot_platform, +) +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration") async def test_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -48,7 +49,8 @@ async def test_entities( assert entity_entry.device_id == device_entry.id -@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") async def test_number_set_value( hass: HomeAssistant, mock_peblar: MagicMock, @@ -73,6 +75,43 @@ async def test_number_set_value( mocked_method.mock_calls[0].assert_called_with({"charge_current_limit": 10}) +async def test_number_set_value_when_charging_is_suspended( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test handling of setting the charging limit while charging is suspended.""" + entity_id = "number.peblar_ev_charger_charge_limit" + + # Suspend charging + mock_peblar.rest_api.return_value.ev_interface.return_value.charge_current_limit = 0 + + # Setup the config entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + # Test normal happy path number value change + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 0 + + # Check the state is reflected + assert (state := hass.states.get(entity_id)) + assert state.state == "10" + + @pytest.mark.parametrize( ("error", "error_match", "translation_key", "translation_placeholders"), [ @@ -96,7 +135,8 @@ async def test_number_set_value( ), ], ) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") async def test_number_set_value_communication_error( hass: HomeAssistant, mock_peblar: MagicMock, @@ -128,6 +168,8 @@ async def test_number_set_value_communication_error( assert excinfo.value.translation_placeholders == translation_placeholders +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration") async def test_number_set_value_authentication_error( hass: HomeAssistant, mock_peblar: MagicMock, @@ -175,3 +217,51 @@ async def test_number_set_value_authentication_error( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == mock_config_entry.entry_id + + +@pytest.mark.parametrize( + ("restore_state", "restore_native_value", "expected_state"), + [ + ("10", 10, "10"), + ("unknown", 10, "unknown"), + ("unavailable", 10, "unknown"), + ("10", None, "unknown"), + ], +) +async def test_restore_state( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + restore_state: str, + restore_native_value: int, + expected_state: str, +) -> None: + """Test restoring the number state.""" + EXTRA_STORED_DATA = { + "native_max_value": 16, + "native_min_value": 6, + "native_step": 1, + "native_unit_of_measurement": "A", + "native_value": restore_native_value, + } + mock_restore_cache_with_extra_data( + hass, + ( + ( + State("number.peblar_ev_charger_charge_limit", restore_state), + EXTRA_STORED_DATA, + ), + ), + ) + + # Adjust Peblar client to have an ignored value for the charging limit + mock_peblar.rest_api.return_value.ev_interface.return_value.charge_current_limit = 0 + + # Setup the config entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Check if state is restored and value is set correctly + assert (state := hass.states.get("number.peblar_ev_charger_charge_limit")) + assert state.state == expected_state diff --git a/tests/components/peblar/test_switch.py b/tests/components/peblar/test_switch.py index 75deeb2d5d3..a7dab51eb3a 100644 --- a/tests/components/peblar/test_switch.py +++ b/tests/components/peblar/test_switch.py @@ -49,10 +49,32 @@ async def test_entities( @pytest.mark.parametrize( - ("service", "force_single_phase"), + ("service", "entity_id", "parameter", "parameter_value"), [ - (SERVICE_TURN_ON, True), - (SERVICE_TURN_OFF, False), + ( + SERVICE_TURN_ON, + "switch.peblar_ev_charger_force_single_phase", + "force_single_phase", + True, + ), + ( + SERVICE_TURN_OFF, + "switch.peblar_ev_charger_force_single_phase", + "force_single_phase", + False, + ), + ( + SERVICE_TURN_ON, + "switch.peblar_ev_charger_charge", + "charge_current_limit", + 16, + ), + ( + SERVICE_TURN_OFF, + "switch.peblar_ev_charger_charge", + "charge_current_limit", + 0, + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -60,10 +82,11 @@ async def test_switch( hass: HomeAssistant, mock_peblar: MagicMock, service: str, - force_single_phase: bool, + entity_id: str, + parameter: str, + parameter_value: bool | int, ) -> None: """Test the Peblar EV charger switches.""" - entity_id = "switch.peblar_ev_charger_force_single_phase" mocked_method = mock_peblar.rest_api.return_value.ev_interface mocked_method.reset_mock() @@ -76,9 +99,7 @@ async def test_switch( ) assert len(mocked_method.mock_calls) == 2 - mocked_method.mock_calls[0].assert_called_with( - {"force_single_phase": force_single_phase} - ) + mocked_method.mock_calls[0].assert_called_with({parameter: parameter_value}) @pytest.mark.parametrize( diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 92ed42aa03a..e0a61106101 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -8,7 +8,6 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from packaging.version import Version -from plugwise import PlugwiseData import pytest from homeassistant.components.plugwise.const import DOMAIN @@ -30,6 +29,15 @@ def _read_json(environment: str, call: str) -> dict[str, Any]: return json.loads(fixture) +@pytest.fixture +def cooling_present(request: pytest.FixtureRequest) -> str: + """Pass the cooling_present boolean. + + Used with fixtures that require parametrization of the cooling capability. + """ + return request.param + + @pytest.fixture def chosen_env(request: pytest.FixtureRequest) -> str: """Pass the chosen_env string. @@ -48,6 +56,24 @@ def gateway_id(request: pytest.FixtureRequest) -> str: return request.param +@pytest.fixture +def heater_id(request: pytest.FixtureRequest) -> str: + """Pass the heater_idstring. + + Used with fixtures that require parametrization of the heater_id. + """ + return request.param + + +@pytest.fixture +def reboot(request: pytest.FixtureRequest) -> str: + """Pass the reboot boolean. + + Used with fixtures that require parametrization of the reboot capability. + """ + return request.param + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" @@ -82,11 +108,14 @@ def mock_smile_config_flow() -> Generator[MagicMock]: autospec=True, ) as smile_mock: smile = smile_mock.return_value + + smile.connect.return_value = Version("4.3.2") smile.smile_hostname = "smile12345" smile.smile_model = "Test Model" smile.smile_model_id = "Test Model ID" smile.smile_name = "Test Smile Name" - smile.connect.return_value = Version("4.3.2") + smile.smile_version = "4.3.2" + yield smile @@ -94,7 +123,7 @@ def mock_smile_config_flow() -> Generator[MagicMock]: def mock_smile_adam() -> Generator[MagicMock]: """Create a Mock Adam environment for testing exceptions.""" chosen_env = "m_adam_multiple_devices_per_zone" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with ( patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True @@ -106,43 +135,45 @@ def mock_smile_adam() -> Generator[MagicMock]: ): smile = smile_mock.return_value + smile.async_update.return_value = data + smile.cooling_present = False + smile.connect.return_value = Version("3.0.15") smile.gateway_id = "fe799307f1624099878210aa0b9f1475" smile.heater_id = "90986d591dcd426cae3ec3e8111ff730" - smile.smile_version = "3.0.15" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_open_therm" smile.smile_name = "Adam" - smile.connect.return_value = Version("3.0.15") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "3.0.15" yield smile @pytest.fixture -def mock_smile_adam_heat_cool(chosen_env: str) -> Generator[MagicMock]: +def mock_smile_adam_heat_cool( + chosen_env: str, cooling_present: bool +) -> Generator[MagicMock]: """Create a special base Mock Adam type for testing with different datasets.""" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.async_update.return_value = data smile.connect.return_value = Version("3.6.4") + smile.cooling_present = cooling_present smile.gateway_id = "da224107914542988a88561b4452b0f6" smile.heater_id = "056ee145a816487eaa69243c3280f8bf" - smile.smile_version = "3.6.4" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_open_therm" smile.smile_name = "Adam" + smile.smile_type = "thermostat" + smile.smile_version = "3.6.4" yield smile @@ -151,49 +182,49 @@ def mock_smile_adam_heat_cool(chosen_env: str) -> Generator[MagicMock]: def mock_smile_adam_jip() -> Generator[MagicMock]: """Create a Mock adam-jip type for testing exceptions.""" chosen_env = "m_adam_jip" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("3.2.8") + smile.cooling_present = False smile.gateway_id = "b5c2386c6f6342669e50fe49dd05b188" smile.heater_id = "e4684553153b44afbef2200885f379dc" - smile.smile_version = "3.2.8" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_open_therm" smile.smile_name = "Adam" - smile.connect.return_value = Version("3.2.8") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "3.2.8" yield smile @pytest.fixture -def mock_smile_anna(chosen_env: str) -> Generator[MagicMock]: +def mock_smile_anna(chosen_env: str, cooling_present: bool) -> Generator[MagicMock]: """Create a Mock Anna type for testing.""" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.async_update.return_value = data smile.connect.return_value = Version("4.0.15") + smile.cooling_present = cooling_present smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" - smile.smile_version = "4.0.15" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_thermo" smile.smile_name = "Smile Anna" + smile.smile_type = "thermostat" + smile.smile_version = "4.0.15" yield smile @@ -201,18 +232,17 @@ def mock_smile_anna(chosen_env: str) -> Generator[MagicMock]: @pytest.fixture def mock_smile_p1(chosen_env: str, gateway_id: str) -> Generator[MagicMock]: """Create a base Mock P1 type for testing with different datasets and gateway-ids.""" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.async_update.return_value = data smile.connect.return_value = Version("4.4.2") smile.gateway_id = gateway_id smile.heater_id = None + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile" @@ -227,24 +257,23 @@ def mock_smile_p1(chosen_env: str, gateway_id: str) -> Generator[MagicMock]: def mock_smile_legacy_anna() -> Generator[MagicMock]: """Create a Mock legacy Anna environment for testing exceptions.""" chosen_env = "legacy_anna" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("1.8.22") smile.gateway_id = "0000aaaa0000aaaa0000aaaa0000aa00" smile.heater_id = "04e4cbfe7f4340f090f85ec3b9e6a950" - smile.smile_version = "1.8.22" - smile.smile_type = "thermostat" + smile.reboot = False smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = None smile.smile_name = "Smile Anna" - smile.connect.return_value = Version("1.8.22") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "1.8.22" yield smile @@ -253,24 +282,23 @@ def mock_smile_legacy_anna() -> Generator[MagicMock]: def mock_stretch() -> Generator[MagicMock]: """Create a Mock Stretch environment for testing exceptions.""" chosen_env = "stretch_v31" - all_data = _read_json(chosen_env, "all_data") + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("3.1.11") smile.gateway_id = "259882df3c05415b99c2d962534ce820" smile.heater_id = None - smile.smile_version = "3.1.11" - smile.smile_type = "stretch" + smile.reboot = False smile.smile_hostname = "stretch98765" smile.smile_model = "Gateway" smile.smile_model_id = None smile.smile_name = "Stretch" - smile.connect.return_value = Version("3.1.11") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "stretch" + smile.smile_version = "3.1.11" yield smile diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json new file mode 100644 index 00000000000..ab6bdf08e95 --- /dev/null +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 20.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": false, + "cooling_state": false, + "dhw_state": false, + "flame_state": false, + "heating_state": true, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 35.0, + "modulation_level": 52, + "outdoor_air_temperature": 3.0, + "return_temperature": 25.1, + "water_pressure": 1.57, + "water_temperature": 29.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "heating", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 19.3 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/legacy_anna/data.json b/tests/components/plugwise/fixtures/legacy_anna/data.json new file mode 100644 index 00000000000..cc7e66fb174 --- /dev/null +++ b/tests/components/plugwise/fixtures/legacy_anna/data.json @@ -0,0 +1,60 @@ +{ + "0000aaaa0000aaaa0000aaaa0000aa00": { + "dev_class": "gateway", + "firmware": "1.8.22", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": "01:23:45:67:89:AB", + "model": "Gateway", + "name": "Smile Anna", + "vendor": "Plugwise" + }, + "04e4cbfe7f4340f090f85ec3b9e6a950": { + "binary_sensors": { + "flame_state": true, + "heating_state": true + }, + "dev_class": "heater_central", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "maximum_boiler_temperature": { + "lower_bound": 50.0, + "resolution": 1.0, + "setpoint": 50.0, + "upper_bound": 90.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 51.2, + "intended_boiler_temperature": 17.0, + "modulation_level": 0.0, + "return_temperature": 21.7, + "water_pressure": 1.2, + "water_temperature": 23.6 + }, + "vendor": "Bosch Thermotechniek B.V." + }, + "0d266432d64443e283b5d708ae98b455": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "heating", + "dev_class": "thermostat", + "firmware": "2017-03-13T11:54:58+01:00", + "hardware": "6539-1301-500", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["away", "vacation", "asleep", "home", "no_frost"], + "sensors": { + "illuminance": 150.8, + "setpoint": 20.5, + "temperature": 20.4 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/data.json b/tests/components/plugwise/fixtures/m_adam_cooling/data.json new file mode 100644 index 00000000000..51f19ca3c03 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_cooling/data.json @@ -0,0 +1,203 @@ +{ + "056ee145a816487eaa69243c3280f8bf": { + "available": true, + "binary_sensors": { + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 50.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 17.5, + "water_temperature": 19.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "1772a4ea304041adb83f357b751341ff": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Badkamer", + "sensors": { + "battery": 99, + "setpoint": 18.0, + "temperature": 21.6, + "temperature_difference": -0.2, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C8FF5EE" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "available": true, + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "ThermoTouch", + "model_id": "143.1", + "name": "Anna", + "sensors": { + "setpoint": 23.5, + "temperature": 25.8 + }, + "vendor": "Plugwise" + }, + "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.7.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345679891", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": [ + "bleeding_hot", + "bleeding_cold", + "off", + "heating", + "cooling" + ], + "select_gateway_mode": "full", + "select_regulation_mode": "cooling", + "sensors": { + "outdoor_temperature": 29.65 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000D5A168D" + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "available": true, + "binary_sensors": { + "low_battery": true + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "model_id": "158-01", + "name": "Lisa Badkamer", + "sensors": { + "battery": 14, + "setpoint": 23.5, + "temperature": 23.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C869B61" + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "f2bf9048bef64cc5b6d5110154e33c81": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "cool", + "control_state": "cooling", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Living room", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 149.9, + "electricity_produced": 0.0, + "temperature": 25.8 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 23.5, + "upper_bound": 35.0 + }, + "thermostats": { + "primary": ["ad4838d7d35c4d6ea796ee12ae5aedf8"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "f871b8c4d63549319221e294e4f88074": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "auto", + "control_state": "cooling", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bathroom", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "Badkamer", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 23.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 25.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["e2f4322d57924fa090fbbc48b3a140dc"], + "secondary": ["1772a4ea304041adb83f357b751341ff"] + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_heating/data.json b/tests/components/plugwise/fixtures/m_adam_heating/data.json new file mode 100644 index 00000000000..b10ff8ec2a8 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating/data.json @@ -0,0 +1,202 @@ +{ + "056ee145a816487eaa69243c3280f8bf": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": true + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 50.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 38.1, + "water_temperature": 37.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "1772a4ea304041adb83f357b751341ff": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Badkamer", + "sensors": { + "battery": 99, + "setpoint": 18.0, + "temperature": 18.6, + "temperature_difference": -0.2, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C8FF5EE" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "available": true, + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "ThermoTouch", + "model_id": "143.1", + "name": "Anna", + "sensors": { + "setpoint": 20.0, + "temperature": 19.1 + }, + "vendor": "Plugwise" + }, + "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.7.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345679891", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": ["bleeding_hot", "bleeding_cold", "off", "heating"], + "select_gateway_mode": "full", + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": -1.25 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000D5A168D" + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "available": true, + "binary_sensors": { + "low_battery": true + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "model_id": "158-01", + "name": "Lisa Badkamer", + "sensors": { + "battery": 14, + "setpoint": 15.0, + "temperature": 17.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C869B61" + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "f2bf9048bef64cc5b6d5110154e33c81": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "heat", + "control_state": "preheating", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Living room", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 149.9, + "electricity_produced": 0.0, + "temperature": 19.1 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 20.0, + "upper_bound": 35.0 + }, + "thermostats": { + "primary": ["ad4838d7d35c4d6ea796ee12ae5aedf8"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "f871b8c4d63549319221e294e4f88074": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bathroom", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "Badkamer", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 17.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["e2f4322d57924fa090fbbc48b3a140dc"], + "secondary": ["1772a4ea304041adb83f357b751341ff"] + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_jip/data.json b/tests/components/plugwise/fixtures/m_adam_jip/data.json new file mode 100644 index 00000000000..8de57910f66 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_jip/data.json @@ -0,0 +1,370 @@ +{ + "06aecb3d00354375924f50c47af36bd2": { + "active_preset": "no_frost", + "climate_mode": "off", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Slaapkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 24.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["1346fbd8498d4dbcab7e18d51b771f3d"], + "secondary": ["356b65335e274d769c338223e7af9c33"] + }, + "vendor": "Plugwise" + }, + "13228dab8ce04617af318a2888b3c548": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Woonkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 27.4 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.01, + "setpoint": 9.0, + "upper_bound": 30.0 + }, + "thermostats": { + "primary": ["f61f1a2535f54f52ad006a3d18e459ca"], + "secondary": ["833de10f269c4deab58fb9df69901b4e"] + }, + "vendor": "Plugwise" + }, + "1346fbd8498d4dbcab7e18d51b771f3d": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "06aecb3d00354375924f50c47af36bd2", + "model": "Lisa", + "model_id": "158-01", + "name": "Slaapkamer", + "sensors": { + "battery": 92, + "setpoint": 13.0, + "temperature": 24.2 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" + }, + "1da4d325838e4ad8aac12177214505c9": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Logeerkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.8, + "temperature_difference": 2.0, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "356b65335e274d769c338223e7af9c33": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "06aecb3d00354375924f50c47af36bd2", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Slaapkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 24.2, + "temperature_difference": 1.7, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" + }, + "457ce8414de24596a2d5e7dbc9c7682f": { + "available": true, + "dev_class": "zz_misc_plug", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "model": "Aqara Smart Plug", + "model_id": "lumi.plug.maeu01", + "name": "Plug", + "sensors": { + "electricity_consumed_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": false + }, + "vendor": "LUMI", + "zigbee_mac_address": "ABCD012345670A06" + }, + "6f3e9d7084214c21b9dfa46f6eeb8700": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "d27aede973b54be484f6842d1b2802ad", + "model": "Lisa", + "model_id": "158-01", + "name": "Kinderkamer", + "sensors": { + "battery": 79, + "setpoint": 13.0, + "temperature": 30.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "833de10f269c4deab58fb9df69901b4e": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "13228dab8ce04617af318a2888b3c548", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Woonkamer", + "sensors": { + "setpoint": 9.0, + "temperature": 24.0, + "temperature_difference": 1.8, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "a6abc6a129ee499c88a4d420cc413b47": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "model": "Lisa", + "model_id": "158-01", + "name": "Logeerkamer", + "sensors": { + "battery": 80, + "setpoint": 13.0, + "temperature": 30.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "b5c2386c6f6342669e50fe49dd05b188": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.2.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], + "select_gateway_mode": "full", + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 24.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "d27aede973b54be484f6842d1b2802ad": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Kinderkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["6f3e9d7084214c21b9dfa46f6eeb8700"], + "secondary": ["d4496250d0e942cfa7aea3476e9070d5"] + }, + "vendor": "Plugwise" + }, + "d4496250d0e942cfa7aea3476e9070d5": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d27aede973b54be484f6842d1b2802ad", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Kinderkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.7, + "temperature_difference": 1.9, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" + }, + "d58fec52899f4f1c92e4f8fad6d8c48c": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Logeerkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["a6abc6a129ee499c88a4d420cc413b47"], + "secondary": ["1da4d325838e4ad8aac12177214505c9"] + }, + "vendor": "Plugwise" + }, + "e4684553153b44afbef2200885f379dc": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 20.0, + "resolution": 0.01, + "setpoint": 90.0, + "upper_bound": 90.0 + }, + "model": "Generic heater", + "model_id": "10.20", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 0.0, + "modulation_level": 0.0, + "return_temperature": 37.1, + "water_pressure": 1.4, + "water_temperature": 37.3 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Remeha B.V." + }, + "f61f1a2535f54f52ad006a3d18e459ca": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermometer", + "firmware": "2020-09-01T02:00:00+02:00", + "hardware": "1", + "location": "13228dab8ce04617af318a2888b3c548", + "model": "Jip", + "model_id": "168-01", + "name": "Woonkamer", + "sensors": { + "battery": 100, + "humidity": 56.2, + "setpoint": 9.0, + "temperature": 27.4 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json new file mode 100644 index 00000000000..7c38b1b2197 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json @@ -0,0 +1,584 @@ +{ + "02cf28bfec924855854c544690a609ef": { + "available": true, + "dev_class": "vcr_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "NVR", + "sensors": { + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A15" + }, + "08963fec7c53423ca5680aa4cb502c63": { + "active_preset": "away", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer Schema", + "sensors": { + "temperature": 18.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 14.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": [ + "f1fee6043d3642a9b0a65297455f008e", + "680423ff840043738f42cc7f1ff97a36" + ], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "12493538af164a409c6a1c79e38afe1c": { + "active_preset": "away", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bios", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 16.5 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["df4a4a8169904cdb9c03d61a21f42140"], + "secondary": ["a2c3583e0a6349358998b760cea82d2a"] + }, + "vendor": "Plugwise" + }, + "21f2b542c49845e6bb416884c55778d6": { + "available": true, + "dev_class": "game_console_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Playstation Smart Plug", + "sensors": { + "electricity_consumed": 84.1, + "electricity_consumed_interval": 8.6, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12" + }, + "446ac08dd04d4eff8ac57489757b7314": { + "active_preset": "no_frost", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Garage", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 15.6 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 5.5, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["e7693eb9582644e5b865dba8d4447cf1"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "4a810418d5394b3f82727340b91ba740": { + "available": true, + "dev_class": "router_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "USG Smart Plug", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16" + }, + "675416a629f343c495449970e2ca37b5": { + "available": true, + "dev_class": "router_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Ziggo Modem", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "680423ff840043738f42cc7f1ff97a36": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Thermostatic Radiator Badkamer 1", + "sensors": { + "battery": 51, + "setpoint": 14.0, + "temperature": 19.1, + "temperature_difference": -0.4, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A17" + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "82fa13f017d240daa0d0ea1775420f24", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Thermostat Jessie", + "sensors": { + "battery": 37, + "setpoint": 15.0, + "temperature": 17.2 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" + }, + "78d1126fc4c743db81b61c20e88342a7": { + "available": true, + "dev_class": "central_heating_pump_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Plug", + "model_id": "160-01", + "name": "CV Pomp", + "sensors": { + "electricity_consumed": 35.6, + "electricity_consumed_interval": 7.37, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" + }, + "82fa13f017d240daa0d0ea1775420f24": { + "active_preset": "asleep", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Jessie", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "CV Jessie", + "sensors": { + "temperature": 17.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["6a3bf693d05e48e0b460c815a4fdd09d"], + "secondary": ["d3da73bde12a47d5a6b8f9dad971f2ec"] + }, + "vendor": "Plugwise" + }, + "90986d591dcd426cae3ec3e8111ff730": { + "binary_sensors": { + "heating_state": true + }, + "dev_class": "heater_central", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "model": "Unknown", + "name": "OnOff", + "sensors": { + "intended_boiler_temperature": 70.0, + "modulation_level": 1, + "water_temperature": 70.0 + } + }, + "a28f588dc4a049a483fd03a30361ad3a": { + "available": true, + "dev_class": "settop_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Fibaro HC2", + "sensors": { + "electricity_consumed": 12.5, + "electricity_consumed_interval": 3.8, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A13" + }, + "a2c3583e0a6349358998b760cea82d2a": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "12493538af164a409c6a1c79e38afe1c", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Bios Cv Thermostatic Radiator ", + "sensors": { + "battery": 62, + "setpoint": 13.0, + "temperature": 17.2, + "temperature_difference": -0.2, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "b310b72a0e354bfab43089919b9a88bf": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Floor kraan", + "sensors": { + "setpoint": 21.5, + "temperature": 26.0, + "temperature_difference": 3.5, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Lisa WK", + "sensors": { + "battery": 34, + "setpoint": 21.5, + "temperature": 20.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "c50f167537524366a5af7aa3942feb1e": { + "active_preset": "home", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "heating", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Woonkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "GF7 Woonkamer", + "sensors": { + "electricity_consumed": 35.6, + "electricity_produced": 0.0, + "temperature": 20.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 21.5, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["b59bcebaf94b499ea7d46e4a66fb62d8"], + "secondary": ["b310b72a0e354bfab43089919b9a88bf"] + }, + "vendor": "Plugwise" + }, + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "available": true, + "dev_class": "vcr_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "NAS", + "sensors": { + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14" + }, + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "82fa13f017d240daa0d0ea1775420f24", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Thermostatic Radiator Jessie", + "sensors": { + "battery": 62, + "setpoint": 15.0, + "temperature": 17.1, + "temperature_difference": 0.1, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A10" + }, + "df4a4a8169904cdb9c03d61a21f42140": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "12493538af164a409c6a1c79e38afe1c", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Lisa Bios", + "sensors": { + "battery": 67, + "setpoint": 13.0, + "temperature": 16.5 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A06" + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "446ac08dd04d4eff8ac57489757b7314", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "CV Kraan Garage", + "sensors": { + "battery": 68, + "setpoint": 5.5, + "temperature": 15.6, + "temperature_difference": 0.0, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A11" + }, + "f1fee6043d3642a9b0a65297455f008e": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Lisa", + "model_id": "158-01", + "name": "Thermostatic Radiator Badkamer 2", + "sensors": { + "battery": 92, + "setpoint": 14.0, + "temperature": 18.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + }, + "fe799307f1624099878210aa0b9f1475": { + "binary_sensors": { + "plugwise_notification": true + }, + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": { + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } + }, + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 7.81 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + } +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json new file mode 100644 index 00000000000..ccfd816ff63 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 28.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": true, + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 41.5, + "intended_boiler_temperature": 0.0, + "modulation_level": 40, + "outdoor_air_temperature": 28.0, + "return_temperature": 23.8, + "water_pressure": 1.57, + "water_temperature": 22.7 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "cooling", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 26.3 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json new file mode 100644 index 00000000000..5a1cdebd380 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 28.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": false, + "cooling_enabled": true, + "cooling_state": false, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 18.0, + "modulation_level": 0, + "outdoor_air_temperature": 28.2, + "return_temperature": 22.0, + "water_pressure": 1.57, + "water_temperature": 19.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 25.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 23.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_single/data.json b/tests/components/plugwise/fixtures/p1v4_442_single/data.json new file mode 100644 index 00000000000..6dfcd7ee033 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_single/data.json @@ -0,0 +1,43 @@ +{ + "a455b61e52394b2db5081ce025a430f3": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "a455b61e52394b2db5081ce025a430f3", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile", + "name": "Smile P1", + "notifications": {}, + "vendor": "Plugwise" + }, + "ba4de7613517478da82dd9b6abea36af": { + "available": true, + "dev_class": "smartmeter", + "location": "a455b61e52394b2db5081ce025a430f3", + "model": "KFM5KAIFA-METER", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 17643.423, + "electricity_consumed_off_peak_interval": 15, + "electricity_consumed_off_peak_point": 486, + "electricity_consumed_peak_cumulative": 13966.608, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 486, + "electricity_phase_one_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_peak_point": 0, + "net_electricity_cumulative": 31610.031, + "net_electricity_point": 486 + }, + "vendor": "SHENZHEN KAIFA TECHNOLOGY \uff08CHENGDU\uff09 CO., LTD." + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/data.json new file mode 100644 index 00000000000..943325d1415 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/data.json @@ -0,0 +1,56 @@ +{ + "03e65b16e4b247a29ae0d75a78cb492e": { + "binary_sensors": { + "plugwise_notification": true + }, + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile", + "name": "Smile P1", + "notifications": { + "97a04c0c263049b29350a660b4cdd01e": { + "warning": "The Smile P1 is not connected to a smart meter." + } + }, + "vendor": "Plugwise" + }, + "b82b6b3322484f2ea4e25e0bd5f3d61f": { + "available": true, + "dev_class": "smartmeter", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "model": "XMX5LGF0010453051839", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 70537.898, + "electricity_consumed_off_peak_interval": 314, + "electricity_consumed_off_peak_point": 5553, + "electricity_consumed_peak_cumulative": 161328.641, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 1763, + "electricity_phase_one_produced": 0, + "electricity_phase_three_consumed": 2080, + "electricity_phase_three_produced": 0, + "electricity_phase_two_consumed": 1703, + "electricity_phase_two_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_peak_point": 0, + "gas_consumed_cumulative": 16811.37, + "gas_consumed_interval": 0.06, + "net_electricity_cumulative": 231866.539, + "net_electricity_point": 5553, + "voltage_phase_one": 233.2, + "voltage_phase_three": 234.7, + "voltage_phase_two": 234.4 + }, + "vendor": "XEMEX NV" + } +} diff --git a/tests/components/plugwise/fixtures/smile_p1_v2/data.json b/tests/components/plugwise/fixtures/smile_p1_v2/data.json new file mode 100644 index 00000000000..768dd2c2334 --- /dev/null +++ b/tests/components/plugwise/fixtures/smile_p1_v2/data.json @@ -0,0 +1,34 @@ +{ + "938696c4bcdb4b8a9a595cb38ed43913": { + "dev_class": "smartmeter", + "location": "938696c4bcdb4b8a9a595cb38ed43913", + "model": "Ene5\\T210-DESMR5.0", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 1642.74, + "electricity_consumed_off_peak_interval": 0, + "electricity_consumed_peak_cumulative": 1155.195, + "electricity_consumed_peak_interval": 250, + "electricity_consumed_point": 458, + "electricity_produced_off_peak_cumulative": 482.598, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_peak_cumulative": 1296.136, + "electricity_produced_peak_interval": 0, + "electricity_produced_point": 0, + "gas_consumed_cumulative": 584.433, + "gas_consumed_interval": 0.016, + "net_electricity_cumulative": 1019.201, + "net_electricity_point": 458 + }, + "vendor": "Ene5\\T210-DESMR5.0" + }, + "aaaa0000aaaa0000aaaa0000aaaa00aa": { + "dev_class": "gateway", + "firmware": "2.5.9", + "location": "938696c4bcdb4b8a9a595cb38ed43913", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/stretch_v31/data.json b/tests/components/plugwise/fixtures/stretch_v31/data.json new file mode 100644 index 00000000000..250839d08a8 --- /dev/null +++ b/tests/components/plugwise/fixtures/stretch_v31/data.json @@ -0,0 +1,136 @@ +{ + "0000aaaa0000aaaa0000aaaa0000aa00": { + "dev_class": "gateway", + "firmware": "3.1.11", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": "01:23:45:67:89:AB", + "model": "Gateway", + "name": "Stretch", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "059e4d03c7a34d278add5c7a4a781d19": { + "dev_class": "washingmachine", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Wasmachine (52AC1)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "5871317346d045bc9f6b987ef25ee638": { + "dev_class": "water_heater_vessel", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4028", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Boiler (1EB31)", + "sensors": { + "electricity_consumed": 1.19, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "aac7b735042c4832ac9ff33aae4f453b": { + "dev_class": "dishwasher", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4022", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Vaatwasser (2a1ab)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.71, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "cfe95cf3de1948c0b8955125bf754614": { + "dev_class": "dryer", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Droger (52559)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" + }, + "d03738edfcc947f7b8f4573571d90d2d": { + "dev_class": "switching", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "cfe95cf3de1948c0b8955125bf754614" + ], + "model": "Switchgroup", + "name": "Schakel", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "d950b314e9d8499f968e6db8d82ef78c": { + "dev_class": "report", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "5871317346d045bc9f6b987ef25ee638", + "aac7b735042c4832ac9ff33aae4f453b", + "cfe95cf3de1948c0b8955125bf754614", + "e1c884e7dede431dadee09506ec4f859" + ], + "model": "Switchgroup", + "name": "Stroomvreters", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "e1c884e7dede431dadee09506ec4f859": { + "dev_class": "refrigerator", + "firmware": "2011-06-27T10:47:37+02:00", + "hardware": "6539-0700-7330", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle+ type F", + "name": "Koelkast (92C4A)", + "sensors": { + "electricity_consumed": 50.5, + "electricity_consumed_interval": 0.08, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "0123456789AB" + } +} diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 806c92fe7cb..92ed327b841 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -1,643 +1,633 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'devices': dict({ - '02cf28bfec924855854c544690a609ef': dict({ - 'available': True, - 'dev_class': 'vcr_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'NVR', - 'sensors': dict({ - 'electricity_consumed': 34.0, - 'electricity_consumed_interval': 9.15, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A15', + '02cf28bfec924855854c544690a609ef': dict({ + 'available': True, + 'dev_class': 'vcr_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'NVR', + 'sensors': dict({ + 'electricity_consumed': 34.0, + 'electricity_consumed_interval': 9.15, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, }), - '08963fec7c53423ca5680aa4cb502c63': dict({ - 'active_preset': 'away', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A15', + }), + '08963fec7c53423ca5680aa4cb502c63': dict({ + 'active_preset': 'away', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Badkamer', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'Badkamer Schema', + 'sensors': dict({ + 'temperature': 18.9, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 14.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'f1fee6043d3642a9b0a65297455f008e', + '680423ff840043738f42cc7f1ff97a36', ]), - 'climate_mode': 'auto', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Badkamer', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'secondary': list([ ]), - 'select_schedule': 'Badkamer Schema', - 'sensors': dict({ - 'temperature': 18.9, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 14.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'f1fee6043d3642a9b0a65297455f008e', - '680423ff840043738f42cc7f1ff97a36', - ]), - 'secondary': list([ - ]), - }), - 'vendor': 'Plugwise', }), - '12493538af164a409c6a1c79e38afe1c': dict({ - 'active_preset': 'away', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'vendor': 'Plugwise', + }), + '12493538af164a409c6a1c79e38afe1c': dict({ + 'active_preset': 'away', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'heat', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Bios', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'off', + 'sensors': dict({ + 'electricity_consumed': 0.0, + 'electricity_produced': 0.0, + 'temperature': 16.5, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 13.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'df4a4a8169904cdb9c03d61a21f42140', ]), - 'climate_mode': 'heat', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Bios', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'secondary': list([ + 'a2c3583e0a6349358998b760cea82d2a', ]), - 'select_schedule': 'off', - 'sensors': dict({ - 'electricity_consumed': 0.0, - 'electricity_produced': 0.0, - 'temperature': 16.5, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 13.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'df4a4a8169904cdb9c03d61a21f42140', - ]), - 'secondary': list([ - 'a2c3583e0a6349358998b760cea82d2a', - ]), - }), - 'vendor': 'Plugwise', }), - '21f2b542c49845e6bb416884c55778d6': dict({ - 'available': True, - 'dev_class': 'game_console_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Playstation Smart Plug', - 'sensors': dict({ - 'electricity_consumed': 84.1, - 'electricity_consumed_interval': 8.6, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': False, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A12', + 'vendor': 'Plugwise', + }), + '21f2b542c49845e6bb416884c55778d6': dict({ + 'available': True, + 'dev_class': 'game_console_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Playstation Smart Plug', + 'sensors': dict({ + 'electricity_consumed': 84.1, + 'electricity_consumed_interval': 8.6, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, }), - '446ac08dd04d4eff8ac57489757b7314': dict({ - 'active_preset': 'no_frost', - 'climate_mode': 'heat', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Garage', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'switches': dict({ + 'lock': False, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A12', + }), + '446ac08dd04d4eff8ac57489757b7314': dict({ + 'active_preset': 'no_frost', + 'climate_mode': 'heat', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Garage', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'sensors': dict({ + 'temperature': 15.6, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 5.5, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'e7693eb9582644e5b865dba8d4447cf1', ]), - 'sensors': dict({ - 'temperature': 15.6, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 5.5, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'e7693eb9582644e5b865dba8d4447cf1', - ]), - 'secondary': list([ - ]), - }), - 'vendor': 'Plugwise', - }), - '4a810418d5394b3f82727340b91ba740': dict({ - 'available': True, - 'dev_class': 'router_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'USG Smart Plug', - 'sensors': dict({ - 'electricity_consumed': 8.5, - 'electricity_consumed_interval': 0.0, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A16', - }), - '675416a629f343c495449970e2ca37b5': dict({ - 'available': True, - 'dev_class': 'router_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Ziggo Modem', - 'sensors': dict({ - 'electricity_consumed': 12.2, - 'electricity_consumed_interval': 2.97, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A01', - }), - '680423ff840043738f42cc7f1ff97a36': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '08963fec7c53423ca5680aa4cb502c63', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Thermostatic Radiator Badkamer 1', - 'sensors': dict({ - 'battery': 51, - 'setpoint': 14.0, - 'temperature': 19.1, - 'temperature_difference': -0.4, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A17', - }), - '6a3bf693d05e48e0b460c815a4fdd09d': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '82fa13f017d240daa0d0ea1775420f24', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Thermostat Jessie', - 'sensors': dict({ - 'battery': 37, - 'setpoint': 15.0, - 'temperature': 17.2, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A03', - }), - '78d1126fc4c743db81b61c20e88342a7': dict({ - 'available': True, - 'dev_class': 'central_heating_pump_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'CV Pomp', - 'sensors': dict({ - 'electricity_consumed': 35.6, - 'electricity_consumed_interval': 7.37, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A05', - }), - '82fa13f017d240daa0d0ea1775420f24': dict({ - 'active_preset': 'asleep', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'secondary': list([ ]), - 'climate_mode': 'auto', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Jessie', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + }), + 'vendor': 'Plugwise', + }), + '4a810418d5394b3f82727340b91ba740': dict({ + 'available': True, + 'dev_class': 'router_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'USG Smart Plug', + 'sensors': dict({ + 'electricity_consumed': 8.5, + 'electricity_consumed_interval': 0.0, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A16', + }), + '675416a629f343c495449970e2ca37b5': dict({ + 'available': True, + 'dev_class': 'router_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Ziggo Modem', + 'sensors': dict({ + 'electricity_consumed': 12.2, + 'electricity_consumed_interval': 2.97, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A01', + }), + '680423ff840043738f42cc7f1ff97a36': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '08963fec7c53423ca5680aa4cb502c63', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Thermostatic Radiator Badkamer 1', + 'sensors': dict({ + 'battery': 51, + 'setpoint': 14.0, + 'temperature': 19.1, + 'temperature_difference': -0.4, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A17', + }), + '6a3bf693d05e48e0b460c815a4fdd09d': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '82fa13f017d240daa0d0ea1775420f24', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Thermostat Jessie', + 'sensors': dict({ + 'battery': 37, + 'setpoint': 15.0, + 'temperature': 17.2, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A03', + }), + '78d1126fc4c743db81b61c20e88342a7': dict({ + 'available': True, + 'dev_class': 'central_heating_pump_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'CV Pomp', + 'sensors': dict({ + 'electricity_consumed': 35.6, + 'electricity_consumed_interval': 7.37, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A05', + }), + '82fa13f017d240daa0d0ea1775420f24': dict({ + 'active_preset': 'asleep', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Jessie', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'CV Jessie', + 'sensors': dict({ + 'temperature': 17.2, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 15.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + '6a3bf693d05e48e0b460c815a4fdd09d', ]), - 'select_schedule': 'CV Jessie', - 'sensors': dict({ - 'temperature': 17.2, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 15.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - '6a3bf693d05e48e0b460c815a4fdd09d', - ]), - 'secondary': list([ - 'd3da73bde12a47d5a6b8f9dad971f2ec', - ]), - }), - 'vendor': 'Plugwise', - }), - '90986d591dcd426cae3ec3e8111ff730': dict({ - 'binary_sensors': dict({ - 'heating_state': True, - }), - 'dev_class': 'heater_central', - 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', - 'model': 'Unknown', - 'name': 'OnOff', - 'sensors': dict({ - 'intended_boiler_temperature': 70.0, - 'modulation_level': 1, - 'water_temperature': 70.0, - }), - }), - 'a28f588dc4a049a483fd03a30361ad3a': dict({ - 'available': True, - 'dev_class': 'settop_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Fibaro HC2', - 'sensors': dict({ - 'electricity_consumed': 12.5, - 'electricity_consumed_interval': 3.8, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A13', - }), - 'a2c3583e0a6349358998b760cea82d2a': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '12493538af164a409c6a1c79e38afe1c', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Bios Cv Thermostatic Radiator ', - 'sensors': dict({ - 'battery': 62, - 'setpoint': 13.0, - 'temperature': 17.2, - 'temperature_difference': -0.2, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A09', - }), - 'b310b72a0e354bfab43089919b9a88bf': dict({ - 'available': True, - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Floor kraan', - 'sensors': dict({ - 'setpoint': 21.5, - 'temperature': 26.0, - 'temperature_difference': 3.5, - 'valve_position': 100, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A02', - }), - 'b59bcebaf94b499ea7d46e4a66fb62d8': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-08-02T02:00:00+02:00', - 'hardware': '255', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Lisa WK', - 'sensors': dict({ - 'battery': 34, - 'setpoint': 21.5, - 'temperature': 20.9, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A07', - }), - 'c50f167537524366a5af7aa3942feb1e': dict({ - 'active_preset': 'home', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'secondary': list([ + 'd3da73bde12a47d5a6b8f9dad971f2ec', ]), - 'climate_mode': 'auto', - 'control_state': 'heating', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Woonkamer', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', - ]), - 'select_schedule': 'GF7 Woonkamer', - 'sensors': dict({ - 'electricity_consumed': 35.6, - 'electricity_produced': 0.0, - 'temperature': 20.9, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 21.5, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'b59bcebaf94b499ea7d46e4a66fb62d8', - ]), - 'secondary': list([ - 'b310b72a0e354bfab43089919b9a88bf', - ]), - }), - 'vendor': 'Plugwise', }), - 'cd0ddb54ef694e11ac18ed1cbce5dbbd': dict({ - 'available': True, - 'dev_class': 'vcr_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'NAS', - 'sensors': dict({ - 'electricity_consumed': 16.5, - 'electricity_consumed_interval': 0.5, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A14', + 'vendor': 'Plugwise', + }), + '90986d591dcd426cae3ec3e8111ff730': dict({ + 'binary_sensors': dict({ + 'heating_state': True, }), - 'd3da73bde12a47d5a6b8f9dad971f2ec': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '82fa13f017d240daa0d0ea1775420f24', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Thermostatic Radiator Jessie', - 'sensors': dict({ - 'battery': 62, - 'setpoint': 15.0, - 'temperature': 17.1, - 'temperature_difference': 0.1, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A10', - }), - 'df4a4a8169904cdb9c03d61a21f42140': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '12493538af164a409c6a1c79e38afe1c', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Lisa Bios', - 'sensors': dict({ - 'battery': 67, - 'setpoint': 13.0, - 'temperature': 16.5, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A06', - }), - 'e7693eb9582644e5b865dba8d4447cf1': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '446ac08dd04d4eff8ac57489757b7314', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'CV Kraan Garage', - 'sensors': dict({ - 'battery': 68, - 'setpoint': 5.5, - 'temperature': 15.6, - 'temperature_difference': 0.0, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A11', - }), - 'f1fee6043d3642a9b0a65297455f008e': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '08963fec7c53423ca5680aa4cb502c63', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Thermostatic Radiator Badkamer 2', - 'sensors': dict({ - 'battery': 92, - 'setpoint': 14.0, - 'temperature': 18.9, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A08', - }), - 'fe799307f1624099878210aa0b9f1475': dict({ - 'binary_sensors': dict({ - 'plugwise_notification': True, - }), - 'dev_class': 'gateway', - 'firmware': '3.0.15', - 'hardware': 'AME Smile 2.0 board', - 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', - 'mac_address': '012345670001', - 'model': 'Gateway', - 'model_id': 'smile_open_therm', - 'name': 'Adam', - 'select_regulation_mode': 'heating', - 'sensors': dict({ - 'outdoor_temperature': 7.81, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670101', + 'dev_class': 'heater_central', + 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', + 'model': 'Unknown', + 'name': 'OnOff', + 'sensors': dict({ + 'intended_boiler_temperature': 70.0, + 'modulation_level': 1, + 'water_temperature': 70.0, }), }), - 'gateway': dict({ - 'cooling_present': False, - 'gateway_id': 'fe799307f1624099878210aa0b9f1475', - 'heater_id': '90986d591dcd426cae3ec3e8111ff730', - 'item_count': 369, + 'a28f588dc4a049a483fd03a30361ad3a': dict({ + 'available': True, + 'dev_class': 'settop_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Fibaro HC2', + 'sensors': dict({ + 'electricity_consumed': 12.5, + 'electricity_consumed_interval': 3.8, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A13', + }), + 'a2c3583e0a6349358998b760cea82d2a': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '12493538af164a409c6a1c79e38afe1c', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Bios Cv Thermostatic Radiator ', + 'sensors': dict({ + 'battery': 62, + 'setpoint': 13.0, + 'temperature': 17.2, + 'temperature_difference': -0.2, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A09', + }), + 'b310b72a0e354bfab43089919b9a88bf': dict({ + 'available': True, + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Floor kraan', + 'sensors': dict({ + 'setpoint': 21.5, + 'temperature': 26.0, + 'temperature_difference': 3.5, + 'valve_position': 100, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A02', + }), + 'b59bcebaf94b499ea7d46e4a66fb62d8': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-08-02T02:00:00+02:00', + 'hardware': '255', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Lisa WK', + 'sensors': dict({ + 'battery': 34, + 'setpoint': 21.5, + 'temperature': 20.9, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A07', + }), + 'c50f167537524366a5af7aa3942feb1e': dict({ + 'active_preset': 'home', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'heating', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Woonkamer', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'GF7 Woonkamer', + 'sensors': dict({ + 'electricity_consumed': 35.6, + 'electricity_produced': 0.0, + 'temperature': 20.9, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 21.5, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'b59bcebaf94b499ea7d46e4a66fb62d8', + ]), + 'secondary': list([ + 'b310b72a0e354bfab43089919b9a88bf', + ]), + }), + 'vendor': 'Plugwise', + }), + 'cd0ddb54ef694e11ac18ed1cbce5dbbd': dict({ + 'available': True, + 'dev_class': 'vcr_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'NAS', + 'sensors': dict({ + 'electricity_consumed': 16.5, + 'electricity_consumed_interval': 0.5, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A14', + }), + 'd3da73bde12a47d5a6b8f9dad971f2ec': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '82fa13f017d240daa0d0ea1775420f24', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Thermostatic Radiator Jessie', + 'sensors': dict({ + 'battery': 62, + 'setpoint': 15.0, + 'temperature': 17.1, + 'temperature_difference': 0.1, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A10', + }), + 'df4a4a8169904cdb9c03d61a21f42140': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '12493538af164a409c6a1c79e38afe1c', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Lisa Bios', + 'sensors': dict({ + 'battery': 67, + 'setpoint': 13.0, + 'temperature': 16.5, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A06', + }), + 'e7693eb9582644e5b865dba8d4447cf1': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '446ac08dd04d4eff8ac57489757b7314', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'CV Kraan Garage', + 'sensors': dict({ + 'battery': 68, + 'setpoint': 5.5, + 'temperature': 15.6, + 'temperature_difference': 0.0, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A11', + }), + 'f1fee6043d3642a9b0a65297455f008e': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '08963fec7c53423ca5680aa4cb502c63', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Thermostatic Radiator Badkamer 2', + 'sensors': dict({ + 'battery': 92, + 'setpoint': 14.0, + 'temperature': 18.9, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A08', + }), + 'fe799307f1624099878210aa0b9f1475': dict({ + 'binary_sensors': dict({ + 'plugwise_notification': True, + }), + 'dev_class': 'gateway', + 'firmware': '3.0.15', + 'hardware': 'AME Smile 2.0 board', + 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', + 'mac_address': '012345670001', + 'model': 'Gateway', + 'model_id': 'smile_open_therm', + 'name': 'Adam', 'notifications': dict({ 'af82e4ccf9c548528166d38e560662a4': dict({ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), - 'reboot': True, - 'smile_name': 'Adam', + 'select_regulation_mode': 'heating', + 'sensors': dict({ + 'outdoor_temperature': 7.81, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670101', }), }) # --- diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 554326a72b1..7bf475086af 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -12,6 +12,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize( ("entity_id", "expected_state"), [ @@ -35,6 +36,7 @@ async def test_anna_climate_binary_sensor_entities( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_binary_sensor_change( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index ab6bd3d4f29..7a481285be0 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -80,6 +80,7 @@ async def test_adam_climate_entity_attributes( @pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) async def test_adam_2_climate_entity_attributes( hass: HomeAssistant, mock_smile_adam_heat_cool: MagicMock, @@ -108,6 +109,7 @@ async def test_adam_2_climate_entity_attributes( @pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_3_climate_entity_attributes( hass: HomeAssistant, mock_smile_adam_heat_cool: MagicMock, @@ -125,18 +127,10 @@ async def test_adam_3_climate_entity_attributes( HVACMode.COOL, ] data = mock_smile_adam_heat_cool.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( - "heating" - ) - data.devices["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = ( - HVACAction.HEATING - ) - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "cooling_state" - ] = False - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "heating_state" - ] = True + data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "heating" + data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.HEATING + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = False + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = True with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -153,18 +147,10 @@ async def test_adam_3_climate_entity_attributes( ] data = mock_smile_adam_heat_cool.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( - "cooling" - ) - data.devices["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = ( - HVACAction.COOLING - ) - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "cooling_state" - ] = True - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "heating_state" - ] = False + data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "cooling" + data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.COOLING + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = True + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = False with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -323,6 +309,7 @@ async def test_adam_climate_off_mode_change( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_entity_attributes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -349,6 +336,7 @@ async def test_anna_climate_entity_attributes( @pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_2_climate_entity_attributes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -369,6 +357,7 @@ async def test_anna_2_climate_entity_attributes( @pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_idle"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_3_climate_entity_attributes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -386,6 +375,7 @@ async def test_anna_3_climate_entity_attributes( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_entity_climate_changes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -441,7 +431,7 @@ async def test_anna_climate_entity_climate_changes( ) data = mock_smile_anna.async_update.return_value - data.devices["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") + data["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 874c4b61a47..5f1f065fa90 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -62,6 +62,7 @@ TOM = { @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -82,6 +83,7 @@ async def test_load_unload_config_entry( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize( ("side_effect", "entry_state"), [ @@ -138,6 +140,7 @@ async def test_device_in_dr( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize( ("entitydata", "old_unique_id", "new_unique_id"), [ @@ -232,6 +235,7 @@ async def test_migrate_unique_id_relay( @pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_update_device( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -265,8 +269,8 @@ async def test_update_device( ) # Add a 2nd Tom/Floor - data.devices.update(TOM) - data.devices["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( + data.update(TOM) + data["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( { "secondary": [ "01234567890abcdefghijklmnopqrstu", @@ -301,10 +305,10 @@ async def test_update_device( assert "01234567890abcdefghijklmnopqrstu" in item_list # Remove the existing Tom/Floor - data.devices["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( + data["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( {"secondary": ["01234567890abcdefghijklmnopqrstu"]} ) - data.devices.pop("1772a4ea304041adb83f357b751341ff") + data.pop("1772a4ea304041adb83f357b751341ff") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index c5361433388..4ae461d96c8 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_number_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -27,6 +28,7 @@ async def test_anna_number_entities( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_max_boiler_temp_change( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -48,6 +50,7 @@ async def test_anna_max_boiler_temp_change( @pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) async def test_adam_dhw_setpoint_change( hass: HomeAssistant, mock_smile_adam_heat_cool: MagicMock, diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index f06d07767f3..f6c4205b756 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -51,6 +51,7 @@ async def test_adam_change_select_entity( @pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_select_regulation_mode( hass: HomeAssistant, mock_smile_adam_heat_cool: MagicMock, @@ -95,6 +96,7 @@ async def test_legacy_anna_select_entities( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_select_unavailable_regulation_mode( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 11aa68bded7..c6c6c6cc284 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -95,6 +95,7 @@ async def test_unique_id_migration_humidity( @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_as_smt_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: 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/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 362adebe416..524aad873f9 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from smhi.smhi_lib import SmhiForecastException +from pysmhi import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -66,7 +66,7 @@ async def test_form(hass: HomeAssistant) -> None: ) with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -102,7 +102,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result2 = await hass.config_entries.flow.async_configure( @@ -122,7 +122,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: # Continue flow with new coordinates with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -170,7 +170,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ): result2 = await hass.config_entries.flow.async_configure( @@ -218,7 +218,7 @@ async def test_reconfigure_flow( assert result["type"] is FlowResultType.FORM with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_reconfigure_flow( with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index d00742d4900..f301e684e3e 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,6 +1,6 @@ """Test SMHI component setup process.""" -from smhi.smhi_lib import APIURL_TEMPLATE +from pysmhi.const import API_POINT_FORECAST from homeassistant.components.smhi.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -17,7 +17,7 @@ async def test_setup_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test setup entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -35,7 +35,7 @@ async def test_remove_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test remove entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -62,7 +62,7 @@ async def test_migrate_entry( api_response: str, ) -> None: """Test migrate entry data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -97,7 +97,7 @@ async def test_migrate_from_future_version( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test migrate entry not possible from future version.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index cc6902710bd..a39cb72d4b8 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -4,8 +4,9 @@ from datetime import datetime, timedelta from unittest.mock import patch from freezegun import freeze_time +from pysmhi import SMHIForecast, SmhiForecastException +from pysmhi.const import API_POINT_FORECAST import pytest -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY @@ -44,7 +45,7 @@ async def test_setup_hass( snapshot: SnapshotAssertion, ) -> None: """Test for successfully setting up the smhi integration.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -54,7 +55,7 @@ async def test_setup_hass( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test @@ -75,7 +76,7 @@ async def test_clear_night( """Test for successfully setting up the smhi integration.""" hass.config.latitude = "59.32624" hass.config.longitude = "17.84197" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_night) @@ -85,7 +86,7 @@ async def test_clear_night( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 state = hass.states.get(ENTITY_ID) @@ -109,7 +110,7 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException("boom"), ): await hass.config_entries.async_setup(entry.entry_id) @@ -134,61 +135,77 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - data = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 0, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data2 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data2 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 12, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data3 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data3 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 2, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 2, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) testdata = [data, data2, data3] @@ -198,11 +215,11 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", return_value=testdata, ), patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_hourly_forecast", return_value=None, ), ): @@ -237,7 +254,7 @@ async def test_refresh_weather_forecast_retry( now = dt_util.utcnow() with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=error, ) as mock_get_forecast: await hass.config_entries.async_setup(entry.entry_id) @@ -352,7 +369,7 @@ async def test_custom_speed_unit( api_response: str, ) -> None: """Test Wind Gust speed with custom unit.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -389,7 +406,7 @@ async def test_forecast_services( snapshot: SnapshotAssertion, ) -> None: """Test multiple forecast.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -440,7 +457,7 @@ async def test_forecast_services( assert msg["type"] == "event" forecast1 = msg["event"]["forecast"] - assert len(forecast1) == 72 + assert len(forecast1) == 52 assert forecast1[0] == snapshot assert forecast1[6] == snapshot @@ -453,7 +470,7 @@ async def test_forecast_services_lack_of_data( snapshot: SnapshotAssertion, ) -> None: """Test forecast lacking data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_lack_data) @@ -498,7 +515,7 @@ async def test_forecast_service( service: str, ) -> None: """Test forecast service.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index be6b5b31325..c9038003cfc 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -184,7 +184,7 @@ async def test_send_message_thread(hass: HomeAssistant, webhook_platform) -> Non assert len(events) == 1 assert events[0].context == context - assert events[0].data[ATTR_MESSAGE_THREAD_ID] == "123" + assert events[0].data[ATTR_MESSAGE_THREAD_ID] == 123 async def test_webhook_endpoint_generates_telegram_text_event( diff --git a/tests/components/vicare/snapshots/test_fan.ambr b/tests/components/vicare/snapshots/test_fan.ambr index 745e77dac5c..b5b02af39b1 100644 --- a/tests/components/vicare/snapshots/test_fan.ambr +++ b/tests/components/vicare/snapshots/test_fan.ambr @@ -1,68 +1,4 @@ # serializer version: 1 -# name: test_all_entities[fan.model0_ventilation-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'preset_modes': list([ - , - , - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.model0_ventilation', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:fan', - 'original_name': 'Ventilation', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': 'ventilation', - 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[fan.model0_ventilation-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Ventilation', - 'icon': 'mdi:fan', - 'percentage': 0, - 'percentage_step': 25.0, - 'preset_mode': None, - 'preset_modes': list([ - , - , - , - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.model0_ventilation', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[fan.model1_ventilation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -92,7 +28,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan', + 'original_icon': 'mdi:fan-off', 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, @@ -106,7 +42,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model1 Ventilation', - 'icon': 'mdi:fan', + 'icon': 'mdi:fan-off', 'percentage': 0, 'percentage_step': 25.0, 'preset_mode': None, 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/conftest.py b/tests/components/zha/conftest.py index 78d335469b8..96a61a6628b 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -155,6 +155,7 @@ async def zigpy_app_controller(): app.state.node_info.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") app.state.node_info.manufacturer = "Coordinator Manufacturer" app.state.node_info.model = "Coordinator Model" + app.state.node_info.version = "7.1.4.0 build 389" app.state.network_info.pan_id = 0x1234 app.state.network_info.extended_pan_id = app.state.node_info.ieee app.state.network_info.channel = 15 diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index f46a06e84b8..c9a5e80b1c9 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -75,7 +75,7 @@ 'manufacturer': 'Coordinator Manufacturer', 'model': 'Coordinator Model', 'nwk': 0, - 'version': None, + 'version': '7.1.4.0 build 389', }), }), 'config': dict({ 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/components/zha/test_homeassistant_hardware.py b/tests/components/zha/test_homeassistant_hardware.py new file mode 100644 index 00000000000..72285521182 --- /dev/null +++ b/tests/components/zha/test_homeassistant_hardware.py @@ -0,0 +1,120 @@ +"""Test Home Assistant Hardware platform for ZHA.""" + +from unittest.mock import MagicMock, patch + +import pytest +from zigpy.application import ControllerApplication + +from homeassistant.components.homeassistant_hardware.helpers import ( + async_register_firmware_info_callback, +) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningIntegration, +) +from homeassistant.components.zha.homeassistant_hardware import get_firmware_info +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_get_firmware_info_normal(hass: HomeAssistant) -> None: + """Test `get_firmware_info`.""" + + zha = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={ + "device": { + "path": "/dev/ttyUSB1", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, + ) + zha.add_to_hass(hass) + zha.mock_state(hass, ConfigEntryState.LOADED) + + # With ZHA running + with patch( + "homeassistant.components.zha.homeassistant_hardware.get_zha_gateway" + ) as mock_get_zha_gateway: + mock_get_zha_gateway.return_value.state.node_info.version = "1.2.3.4" + fw_info_running = get_firmware_info(hass, zha) + + assert fw_info_running == FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version="1.2.3.4", + source="zha", + owners=[OwningIntegration(config_entry_id=zha.entry_id)], + ) + assert await fw_info_running.is_running(hass) is True + + # With ZHA not running + zha.mock_state(hass, ConfigEntryState.NOT_LOADED) + fw_info_not_running = get_firmware_info(hass, zha) + + assert fw_info_not_running == FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="zha", + owners=[OwningIntegration(config_entry_id=zha.entry_id)], + ) + assert await fw_info_not_running.is_running(hass) is False + + +@pytest.mark.parametrize( + "data", + [ + # Missing data + {}, + # Bad radio type + {"device": {"path": "/dev/ttyUSB1"}, "radio_type": "znp"}, + ], +) +async def test_get_firmware_info_errors( + hass: HomeAssistant, data: dict[str, str | int | None] +) -> None: + """Test `get_firmware_info` with config entry data format errors.""" + zha = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data=data, + version=4, + ) + zha.add_to_hass(hass) + + assert (get_firmware_info(hass, zha)) is None + + +async def test_hardware_firmware_info_provider_notification( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, +) -> None: + """Test that the ZHA gateway provides hardware and firmware information.""" + config_entry.add_to_hass(hass) + + await async_setup_component(hass, "homeassistant_hardware", {}) + + callback = MagicMock() + async_register_firmware_info_callback(hass, "/dev/ttyUSB0", callback) + + await hass.config_entries.async_setup(config_entry.entry_id) + + callback.assert_called_once_with( + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.EZSP, + firmware_version="7.1.4.0 build 389", + source="zha", + owners=[OwningIntegration(config_entry_id=config_entry.entry_id)], + ) + ) 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" diff --git a/tests/test_setup.py b/tests/test_setup.py index 2d15c670cf7..bb221c7cb4c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -363,20 +363,24 @@ async def test_component_failing_setup(hass: HomeAssistant) -> None: async def test_component_exception_setup(hass: HomeAssistant) -> None: """Test component that raises exception during setup.""" - setup.async_set_domains_to_be_loaded(hass, {"comp"}) + domain = "comp" + setup.async_set_domains_to_be_loaded(hass, {domain}) def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Raise exception.""" raise Exception("fail!") # noqa: TRY002 - mock_integration(hass, MockModule("comp", setup=exception_setup)) + mock_integration(hass, MockModule(domain, setup=exception_setup)) - assert not await setup.async_setup_component(hass, "comp", {}) - assert "comp" not in hass.config.components + assert not await setup.async_setup_component(hass, domain, {}) + assert domain in hass.data[setup.DATA_SETUP] + assert domain not in hass.data[setup.DATA_SETUP_DONE] + assert domain not in hass.config.components async def test_component_base_exception_setup(hass: HomeAssistant) -> None: """Test component that raises exception during setup.""" + domain = "comp" setup.async_set_domains_to_be_loaded(hass, {"comp"}) def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -389,7 +393,69 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: await setup.async_setup_component(hass, "comp", {}) assert str(exc_info.value) == "fail!" - assert "comp" not in hass.config.components + assert domain in hass.data[setup.DATA_SETUP] + assert domain not in hass.data[setup.DATA_SETUP_DONE] + assert domain not in hass.config.components + + +async def test_set_domains_to_be_loaded(hass: HomeAssistant) -> None: + """Test async_set_domains_to_be_loaded.""" + domain_good = "comp_good" + domain_bad = "comp_bad" + domain_base_exception = "comp_base_exception" + domain_exception = "comp_exception" + domains = {domain_good, domain_bad, domain_exception, domain_base_exception} + setup.async_set_domains_to_be_loaded(hass, domains) + + assert set(hass.data[setup.DATA_SETUP_DONE]) == domains + setup_done = dict(hass.data[setup.DATA_SETUP_DONE]) + + # Calling async_set_domains_to_be_loaded again should not create new futures + setup.async_set_domains_to_be_loaded(hass, domains) + assert setup_done == hass.data[setup.DATA_SETUP_DONE] + + def good_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Success.""" + return True + + def bad_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Fail.""" + return False + + def base_exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Raise exception.""" + raise BaseException("fail!") # noqa: TRY002 + + def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Raise exception.""" + raise Exception("fail!") # noqa: TRY002 + + mock_integration(hass, MockModule(domain_good, setup=good_setup)) + mock_integration(hass, MockModule(domain_bad, setup=bad_setup)) + mock_integration( + hass, MockModule(domain_base_exception, setup=base_exception_setup) + ) + mock_integration(hass, MockModule(domain_exception, setup=exception_setup)) + + # Set up the four components + assert await setup.async_setup_component(hass, domain_good, {}) + assert not await setup.async_setup_component(hass, domain_bad, {}) + assert not await setup.async_setup_component(hass, domain_exception, {}) + with pytest.raises(BaseException, match="fail!"): + await setup.async_setup_component(hass, domain_base_exception, {}) + + # Check the result of the setup + assert not hass.data[setup.DATA_SETUP_DONE] + assert set(hass.data[setup.DATA_SETUP]) == { + domain_bad, + domain_exception, + domain_base_exception, + } + assert set(hass.config.components) == {domain_good} + + # Calling async_set_domains_to_be_loaded again should not create any new futures + setup.async_set_domains_to_be_loaded(hass, domains) + assert not hass.data[setup.DATA_SETUP_DONE] async def test_component_setup_with_validation_and_dependency(