From 43239682226109baacc4149873b0d629146d05aa Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 11:13:55 +0100 Subject: [PATCH 01/61] Replace "HassOS" with "Home Assistant OS" in homeassistant_hardware (#137637) Makes it consistent with all other occurrences. Also change "otbr" and "zha" to uppercase. --- homeassistant/components/homeassistant_hardware/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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": { From f06d209b2dc41880fc294f335059d95c3745441e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:14:45 +0100 Subject: [PATCH 02/61] Improve type hints in fireservicerota (#137628) --- .../components/fireservicerota/coordinator.py | 8 +++++--- homeassistant/components/fireservicerota/sensor.py | 9 +++++---- homeassistant/components/fireservicerota/switch.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fireservicerota/coordinator.py b/homeassistant/components/fireservicerota/coordinator.py index 35f839b3bdb..14a8c40e469 100644 --- a/homeassistant/components/fireservicerota/coordinator.py +++ b/homeassistant/components/fireservicerota/coordinator.py @@ -54,7 +54,9 @@ class FireServiceUpdateCoordinator(DataUpdateCoordinator[dict | None]): class FireServiceRotaOauth: """Handle authentication tokens.""" - def __init__(self, hass, entry, fsr): + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, fsr: FireServiceRota + ) -> None: """Initialize the oauth object.""" self._hass = hass self._entry = entry @@ -94,7 +96,7 @@ class FireServiceRotaOauth: class FireServiceRotaWebSocket: """Define a FireServiceRota websocket manager object.""" - def __init__(self, hass, entry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the websocket object.""" self._hass = hass self._entry = entry @@ -128,7 +130,7 @@ class FireServiceRotaWebSocket: class FireServiceRotaClient: """Getting the latest data from fireservicerota.""" - def __init__(self, hass, entry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the data object.""" self._hass = hass self._entry = entry diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 864838ddaff..b09d1295025 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN +from .coordinator import FireServiceRotaClient _LOGGER = logging.getLogger(__name__) @@ -32,13 +33,13 @@ class IncidentsSensor(RestoreEntity, SensorEntity): _attr_has_entity_name = True _attr_translation_key = "incidents" - def __init__(self, client): + def __init__(self, client: FireServiceRotaClient) -> None: """Initialize.""" self._client = client self._entry_id = self._client.entry_id self._attr_unique_id = f"{self._client.unique_id}_Incidents" - self._state = None - self._state_attributes = {} + self._state: str | None = None + self._state_attributes: dict[str, Any] = {} @property def icon(self) -> str: @@ -52,7 +53,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): return "mdi:fire-truck" @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 22287653788..affd46c91bd 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -10,6 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN +from .coordinator import FireServiceRotaClient, FireServiceUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,15 +33,20 @@ class ResponseSwitch(SwitchEntity): _attr_has_entity_name = True _attr_translation_key = "incident_response" - def __init__(self, coordinator, client, entry): + def __init__( + self, + coordinator: FireServiceUpdateCoordinator, + client: FireServiceRotaClient, + entry: ConfigEntry, + ) -> None: """Initialize.""" self._coordinator = coordinator self._client = client self._attr_unique_id = f"{entry.unique_id}_Response" self._entry_id = entry.entry_id - self._state = None - self._state_attributes = {} + self._state: bool | None = None + self._state_attributes: dict[str, Any] = {} self._state_icon = None @property @@ -54,7 +60,7 @@ class ResponseSwitch(SwitchEntity): return "mdi:forum" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get the assumed state of the switch.""" return self._state From 60fd07f501044ecfbe008546ffe471c501467833 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:26:25 +0100 Subject: [PATCH 03/61] Use runtime_data in frontier_silicon (#137633) --- .../components/frontier_silicon/__init__.py | 19 +++++++++++-------- .../frontier_silicon/media_player.py | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) 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( [ From ff42353e61aeec0072b937903fdf8eb83b07d77f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:27:41 +0100 Subject: [PATCH 04/61] Use runtime_data in fivem (#137632) --- homeassistant/components/fivem/__init__.py | 17 +++++--------- .../components/fivem/binary_sensor.py | 8 +++---- homeassistant/components/fivem/coordinator.py | 22 ++++++++++++------- homeassistant/components/fivem/sensor.py | 7 +++--- 4 files changed, 27 insertions(+), 27 deletions(-) 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( From bdc847c7ac24c2de7ab81f71fb45fdd6c28ccea2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:30:13 +0100 Subject: [PATCH 05/61] Use runtime_data in firmata (#137630) --- homeassistant/components/firmata/__init__.py | 21 ++++++++++--------- .../components/firmata/binary_sensor.py | 8 +++---- homeassistant/components/firmata/light.py | 7 ++++--- homeassistant/components/firmata/sensor.py | 8 +++---- homeassistant/components/firmata/switch.py | 8 +++---- 5 files changed, 27 insertions(+), 25 deletions(-) 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] From d9726ab08c705fd80f73e794273c2f9cc54c9c8b Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 7 Feb 2025 11:32:47 +0100 Subject: [PATCH 06/61] Use snapshots for ConfigEntry migration tests (#136093) * Add snapshots for migration * Reduce fixtures specific to migration * Explicitly test versions of migrated entries --- .../lcn/fixtures/config_entry_pchk_v1_1.json | 217 ------------------ .../lcn/fixtures/config_entry_pchk_v1_2.json | 184 +-------------- tests/components/lcn/snapshots/test_init.ambr | 140 +++++++++++ tests/components/lcn/test_init.py | 11 +- 4 files changed, 148 insertions(+), 404 deletions(-) create mode 100644 tests/components/lcn/snapshots/test_init.ambr 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 From 4c9127a0ea1d0ac5c74018d64d6d17f2d98295c5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:28:44 +0100 Subject: [PATCH 07/61] Remove unnecessary type casts (#137657) --- homeassistant/components/command_line/binary_sensor.py | 3 --- homeassistant/components/command_line/cover.py | 3 +-- homeassistant/components/command_line/notify.py | 3 +-- homeassistant/components/command_line/sensor.py | 4 +--- homeassistant/components/command_line/switch.py | 3 +-- homeassistant/components/mealie/services.py | 2 +- homeassistant/components/nws/weather.py | 4 +--- 7 files changed, 6 insertions(+), 16 deletions(-) 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/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/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) ) From d6d9c9f01a386d48a4105106d8aa3a6af78c6ce1 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 7 Feb 2025 12:49:45 +0100 Subject: [PATCH 08/61] Bump PyTado to version 0.18.6 (#137655) --- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 4c3c2b42424..56e7ddf173a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2464,7 +2464,7 @@ python-smarttub==0.0.38 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.5 +python-tado==0.18.6 # homeassistant.components.technove python-technove==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef9b2978459..f051dde3162 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1997,7 +1997,7 @@ python-smarttub==0.0.38 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.5 +python-tado==0.18.6 # homeassistant.components.technove python-technove==1.3.1 From da0481852ee467a9082083174a85c30625748c2c Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 13:05:32 +0100 Subject: [PATCH 09/61] Make all occurrences of "Home Guard" in lg_thinq consistent (#137662) LQ uses "Home Guard" as a trademark, so all occurrences in the integration should be consistent in spelling. --- homeassistant/components/lg_thinq/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 8f498e0f8a2..dee2d21e05a 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -215,9 +215,9 @@ "error_during_washing": "An error has occurred in the washing machine", "error_has_occurred": "An error has occurred", "frozen_is_complete": "Ice plus is done", - "homeguard_is_stopped": "Home guard has stopped", + "homeguard_is_stopped": "Home Guard has stopped", "lack_of_water": "There is no water in the water tank", - "motion_is_detected": "Photograph is sent as movement is detected during home guard", + "motion_is_detected": "Photograph is sent as movement is detected during Home Guard", "need_to_check_location": "Location check is required", "pollution_is_high": "Air status is rapidly becoming bad", "preheating_is_complete": "Preheating is done", @@ -432,9 +432,9 @@ "lock": "Control lock", "macrosector": "Remote is in use", "melting": "Wort dissolving", - "monitoring_detecting": "HomeGuard is active", + "monitoring_detecting": "Home Guard is active", "monitoring_moving": "Going to the starting point", - "monitoring_positioning": "Setting homeguard start point", + "monitoring_positioning": "Setting Home Guard start point", "night_dry": "Night dry", "oven_setting": "Cooktop connected", "pause": "[%key:common::state::paused%]", From e340f5af8d2e3999eb0bbe890a38a63280ac7afa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:06:13 +0100 Subject: [PATCH 10/61] Use runtime_data in flume (#137660) --- homeassistant/components/flume/__init__.py | 50 +++++++------------ .../components/flume/binary_sensor.py | 13 ++--- homeassistant/components/flume/const.py | 6 --- homeassistant/components/flume/coordinator.py | 16 ++++++ homeassistant/components/flume/sensor.py | 17 +++---- 5 files changed, 46 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index d91c6b175cf..9da0e5163c5 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -7,7 +7,7 @@ from requests import Session from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -24,16 +24,12 @@ 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" @@ -76,7 +72,7 @@ def _setup_entry( return flume_auth, flume_devices, http_session -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FlumeConfigEntry) -> bool: """Set up flume from a config entry.""" flume_auth, flume_devices, http_session = await hass.async_add_executor_job( @@ -86,12 +82,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass=hass, auth=flume_auth ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - FLUME_DEVICES: flume_devices, - FLUME_AUTH: flume_auth, - FLUME_HTTP_SESSION: http_session, - FLUME_NOTIFICATIONS_COORDINATOR: notification_coordinator, - } + entry.runtime_data = FlumeRuntimeData( + devices=flume_devices, + auth=flume_auth, + http_session=http_session, + notifications_coordinator=notification_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) setup_service(hass) @@ -99,16 +95,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: FlumeConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - hass.data[DOMAIN][entry.entry_id][FLUME_HTTP_SESSION].close() - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + entry.runtime_data.http_session.close() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) def setup_service(hass: HomeAssistant) -> None: @@ -118,15 +108,13 @@ def setup_service(hass: HomeAssistant) -> None: def list_notifications(call: ServiceCall) -> ServiceResponse: """Return the user notifications.""" entry_id: str = call.data[CONF_CONFIG_ENTRY] - entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id) + entry: FlumeConfigEntry | None = hass.config_entries.async_get_entry(entry_id) if not entry: raise ValueError(f"Invalid config entry: {entry_id}") - if not (flume_domain_data := hass.data[DOMAIN].get(entry_id)): + if not entry.state == ConfigEntryState.LOADED: raise ValueError(f"Config entry not loaded: {entry_id}") return { - "notifications": flume_domain_data[ - FLUME_NOTIFICATIONS_COORDINATOR - ].notifications + "notifications": entry.runtime_data.notifications_coordinator.notifications # type: ignore[dict-item] } hass.services.async_register( diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 28f56168d9c..67cb71c5767 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -9,15 +9,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DOMAIN, - FLUME_DEVICES, - FLUME_NOTIFICATIONS_COORDINATOR, FLUME_TYPE_BRIDGE, FLUME_TYPE_SENSOR, KEY_DEVICE_ID, @@ -29,6 +25,7 @@ from .const import ( NOTIFICATION_LOW_BATTERY, ) from .coordinator import ( + FlumeConfigEntry, FlumeDeviceConnectionUpdateCoordinator, FlumeNotificationDataUpdateCoordinator, ) @@ -71,12 +68,12 @@ FLUME_BINARY_NOTIFICATION_SENSORS: tuple[FlumeBinarySensorEntityDescription, ... async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FlumeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Flume binary sensor..""" - flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] - flume_devices = flume_domain_data[FLUME_DEVICES] + flume_domain_data = config_entry.runtime_data + flume_devices = flume_domain_data.devices flume_entity_list: list[ FlumeNotificationBinarySensor | FlumeConnectionBinarySensor @@ -85,7 +82,7 @@ async def async_setup_entry( connection_coordinator = FlumeDeviceConnectionUpdateCoordinator( hass=hass, flume_devices=flume_devices ) - notification_coordinator = flume_domain_data[FLUME_NOTIFICATIONS_COORDINATOR] + notification_coordinator = flume_domain_data.notifications_coordinator flume_devices = get_valid_flume_devices(flume_devices) for device in flume_devices: device_id = device[KEY_DEVICE_ID] diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 1f9fc10b1b3..a8fe21f4b06 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -26,12 +26,6 @@ _LOGGER = logging.getLogger(__package__) FLUME_TYPE_BRIDGE = 1 FLUME_TYPE_SENSOR = 2 - -FLUME_AUTH = "flume_auth" -FLUME_HTTP_SESSION = "http_session" -FLUME_DEVICES = "devices" -FLUME_NOTIFICATIONS_COORDINATOR = "notifications_coordinator" - CONF_TOKEN_FILE = "token_filename" BASE_TOKEN_FILENAME = "FLUME_TOKEN_FILE" diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 30e7962304c..fc76600cad4 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -2,11 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any import pyflume from pyflume import FlumeAuth, FlumeData, FlumeDeviceList +from requests import Session +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,6 +22,19 @@ from .const import ( ) +@dataclass +class FlumeRuntimeData: + """Runtime data for the Flume config entry.""" + + devices: FlumeDeviceList + auth: FlumeAuth + http_session: Session + notifications_coordinator: FlumeNotificationDataUpdateCoordinator + + +type FlumeConfigEntry = ConfigEntry[FlumeRuntimeData] + + class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for an individual flume device.""" diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 96395e5403f..6c7cc0ab37d 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,10 +18,6 @@ from homeassistant.helpers.typing import StateType from .const import ( DEVICE_SCAN_INTERVAL, - DOMAIN, - FLUME_AUTH, - FLUME_DEVICES, - FLUME_HTTP_SESSION, FLUME_TYPE_SENSOR, KEY_DEVICE_ID, KEY_DEVICE_LOCATION, @@ -30,7 +25,7 @@ from .const import ( KEY_DEVICE_LOCATION_TIMEZONE, KEY_DEVICE_TYPE, ) -from .coordinator import FlumeDeviceDataUpdateCoordinator +from .coordinator import FlumeConfigEntry, FlumeDeviceDataUpdateCoordinator from .entity import FlumeEntity from .util import get_valid_flume_devices @@ -112,15 +107,15 @@ def make_flume_datas( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FlumeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Flume sensor.""" - flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] - flume_devices = flume_domain_data[FLUME_DEVICES] - flume_auth: FlumeAuth = flume_domain_data[FLUME_AUTH] - http_session: Session = flume_domain_data[FLUME_HTTP_SESSION] + flume_domain_data = config_entry.runtime_data + flume_devices = flume_domain_data.devices + flume_auth = flume_domain_data.auth + http_session = flume_domain_data.http_session flume_devices = [ device for device in get_valid_flume_devices(flume_devices) From 6d6961ae6ef2ec0624ee4388da37e54803f817df Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:44:44 +0100 Subject: [PATCH 11/61] Clean up colliding deleted devices when updating non-deleted devices (#135592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Schrödinger's devices * Address feedback * Add comment with broader context --- homeassistant/helpers/device_registry.py | 42 ++++++++++++++++++++++-- tests/helpers/test_device_registry.py | 33 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) 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/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, From 239f2dcb3e5ab8cb297908aa55850fe07fac2a1d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 7 Feb 2025 16:25:09 +0200 Subject: [PATCH 12/61] Fix LG webOS TV turn off when device is already off (#137675) --- homeassistant/components/webostv/media_player.py | 2 +- tests/components/webostv/test_media_player.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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/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" From 600269ad0aac17a18282b8b46126bd4550af47a5 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 15:33:38 +0100 Subject: [PATCH 13/61] Replace key names with friendly names in todoist actions (#137667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace key names with friendly names in todoist actions Also fix the grammar mistake in "A members username …" by rewordings. * Fix grammar in reminder_date and reminder_date_string descriptions * Fix description of due_date_string, replacing "date" with "time" According to the online docs this is not the day but the time when this is due. Just like the reminder_date_string. Example in the docs: "tomorrow at 14:00" --- homeassistant/components/todoist/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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." } } } From dd82212e4501da21944b488f6d5e57378e66f5b2 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:04:34 -0600 Subject: [PATCH 14/61] Handle previously migrated HEOS device identifier (#137596) --- homeassistant/components/heos/__init__.py | 19 ++++++++++++++--- tests/components/heos/test_init.py | 25 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index d735469c5cb..0c268b612ea 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -39,9 +39,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool ): for domain, player_id in device.identifiers: if domain == DOMAIN and not isinstance(player_id, str): - device_registry.async_update_device( # type: ignore[unreachable] - device.id, new_identifiers={(DOMAIN, str(player_id))} - ) + # Create set of identifiers excluding this integration + identifiers = { # type: ignore[unreachable] + (domain, identifier) + for domain, identifier in device.identifiers + if domain != DOMAIN + } + migrated_identifiers = {(DOMAIN, str(player_id))} + # Add migrated if not already present in another device, which occurs if the user downgraded and then upgraded + if not device_registry.async_get_device(migrated_identifiers): + identifiers.update(migrated_identifiers) + if len(identifiers) > 0: + device_registry.async_update_device( + device.id, new_identifiers=identifiers + ) + else: + device_registry.async_remove_device(device.id) break coordinator = HeosCoordinator(hass, entry) diff --git a/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 From 040e1ff5fbf51d94fdc0d9d447604d43900e1ac1 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 7 Feb 2025 16:06:33 +0100 Subject: [PATCH 15/61] Use separate metadata files for onedrive (#137549) --- homeassistant/components/onedrive/__init__.py | 43 ++++++++- homeassistant/components/onedrive/backup.py | 95 ++++++++++++------- .../components/onedrive/strings.json | 3 + tests/components/onedrive/conftest.py | 10 +- tests/components/onedrive/const.py | 25 ++++- tests/components/onedrive/test_backup.py | 2 +- tests/components/onedrive/test_init.py | 37 ++++++++ 7 files changed, 178 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/onedrive/__init__.py b/homeassistant/components/onedrive/__init__.py index 5feefb2cf7d..9716f692ec8 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), # type: ignore[arg-type] + ) + 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..182e29aa63f 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, # type: ignore[arg-type] + ) + # 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/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/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 From 1d6aec44dfd365073a50e4bfe76799fd87daf5fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:30:58 +0100 Subject: [PATCH 16/61] Use config_entry.async_on_unload in forked_daapd (#137656) --- .../components/forked_daapd/__init__.py | 6 +----- homeassistant/components/forked_daapd/const.py | 1 - .../components/forked_daapd/media_player.py | 16 ++++++---------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 6ff9a030a02..2172e60ba38 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY +from .const import DOMAIN, HASS_DATA_UPDATER_KEY PLATFORMS = [Platform.MEDIA_PLAYER] @@ -23,10 +23,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: HASS_DATA_UPDATER_KEY ].websocket_handler: websocket_handler.cancel() - for remove_listener in hass.data[DOMAIN][entry.entry_id][ - HASS_DATA_REMOVE_LISTENERS_KEY - ]: - remove_listener() del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: del hass.data[DOMAIN] diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 8d671f2fc07..dd7ed1bdf16 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -32,7 +32,6 @@ DEFAULT_TTS_VOLUME = 0.8 DEFAULT_UNMUTE_VOLUME = 0.6 DOMAIN = "forked_daapd" # key for hass.data FD_NAME = "OwnTone" -HASS_DATA_REMOVE_LISTENERS_KEY = "REMOVE_LISTENERS" HASS_DATA_UPDATER_KEY = "UPDATER" KNOWN_PIPES = {"librespot-java"} PIPE_FUNCTION_MAP = { diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index b8b544c1a2c..0116cc57e7b 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -58,7 +58,6 @@ from .const import ( DEFAULT_UNMUTE_VOLUME, DOMAIN, FD_NAME, - HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY, KNOWN_PIPES, PIPE_FUNCTION_MAP, @@ -110,19 +109,16 @@ async def async_setup_entry( ForkedDaapdZone(api, output, config_entry.entry_id) for output in outputs ) - remove_add_zones_listener = async_dispatcher_connect( - hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones + config_entry.async_on_unload( + async_dispatcher_connect( + hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones + ) ) - remove_entry_listener = config_entry.add_update_listener(update_listener) + config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) if not hass.data.get(DOMAIN): hass.data[DOMAIN] = {config_entry.entry_id: {}} - hass.data[DOMAIN][config_entry.entry_id] = { - HASS_DATA_REMOVE_LISTENERS_KEY: [ - remove_add_zones_listener, - remove_entry_listener, - ] - } + async_add_entities([forked_daapd_master], False) forked_daapd_updater = ForkedDaapdUpdater( hass, forked_daapd_api, config_entry.entry_id From b9dccb91255cfbe44f03b159b49cc9950a3886a3 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 16:32:03 +0100 Subject: [PATCH 17/61] Fix spelling of "SwitchBot", "ID" plus sentence-casing in switchbot (#137684) Fix spelling of "SwitchBot", "ID" and sentence-case "encryption" --- homeassistant/components/switchbot/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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": { From aa6fa3cdadd5fec214a6ca8c13539f29ae38b50a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 Feb 2025 16:32:28 +0100 Subject: [PATCH 18/61] Don't use the current temperature from Shelly BLU TRV as a state for External Temperature number entity (#137658) Introduce RpcBluTrvExtTempNumber for External Temperature entity --- homeassistant/components/shelly/number.py | 20 ++++++- .../shelly/snapshots/test_number.ambr | 2 +- tests/components/shelly/test_number.py | 56 ++++++++++++++++--- 3 files changed, 68 insertions(+), 10 deletions(-) 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/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" From b3205ea1cd4bb43ba31f04df1a879a9922e99d39 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:33:40 -0500 Subject: [PATCH 19/61] Do not rely on pyserial for port scanning with the CM5 + ZHA (#137585) Do not rely on pyserial for port scanning with the CM5 --- homeassistant/components/zha/config_flow.py | 11 ++++++++--- tests/components/zha/test_config_flow.py | 13 +++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) 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/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 From 6ff733b2250f1e92dbc6ec992329d75fc9dcf75b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 Feb 2025 16:36:26 +0100 Subject: [PATCH 20/61] Set the device class for the Shelly virtual sensor (#137068) * Map sensor role to the device class * Add test * Fix docstring --- homeassistant/components/shelly/const.py | 6 +++++ homeassistant/components/shelly/sensor.py | 14 ++++++++++- tests/components/shelly/test_sensor.py | 29 +++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) 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/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/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 From 9a202b5e5613a65bd559183384dde1cb7a2c837b Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 18:47:05 +0100 Subject: [PATCH 21/61] Fix spelling of "AccuWeather" and sentence-casing plus grammar (#137696) --- homeassistant/components/accuweather/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": { From 2eb6a03640012b8b9d318b461a453dc8f0bcf794 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:38:41 +0100 Subject: [PATCH 22/61] Pass in the config_entry in azure_devops coordinator init (#137722) explicitly pass in the config_entry in azure_devops coordinator init --- homeassistant/components/azure_devops/__init__.py | 10 ++-------- homeassistant/components/azure_devops/coordinator.py | 11 +++++++---- homeassistant/components/azure_devops/sensor.py | 3 +-- 3 files changed, 10 insertions(+), 14 deletions(-) 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__) From 1f0f1ac388047dcc9f71be2f6070e78760b57d8a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:39:21 +0100 Subject: [PATCH 23/61] Explicitly pass in the config_entry in autarco coordinator init (#137718) explicitly pass in the config_entry in autarco coordinator init --- homeassistant/components/autarco/__init__.py | 8 +++----- homeassistant/components/autarco/coordinator.py | 6 +++++- homeassistant/components/autarco/diagnostics.py | 2 +- homeassistant/components/autarco/sensor.py | 3 +-- 4 files changed, 10 insertions(+), 9 deletions(-) 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) From 42169e28a48a5dd68d918f761e3b653a7e343e25 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:45:23 +0100 Subject: [PATCH 24/61] Explicitly pass in the config_entry in airnow coordinator init (#137699) explicitly pass in the config_entry in airnow coordinator init --- homeassistant/components/airnow/__init__.py | 6 ++---- homeassistant/components/airnow/coordinator.py | 14 +++++++++++++- homeassistant/components/airnow/diagnostics.py | 2 +- homeassistant/components/airnow/sensor.py | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) 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" From a8de073076d911c2e385196dc897f0ede4e5e296 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:46:21 +0100 Subject: [PATCH 25/61] Explicitly pass in the config_entry in airly coordinator init (#137698) explicitly pass in the config_entry in airly coordinator init --- homeassistant/components/airly/__init__.py | 14 +++++++++----- homeassistant/components/airly/coordinator.py | 14 +++++++++++++- homeassistant/components/airly/diagnostics.py | 2 +- homeassistant/components/airly/sensor.py | 2 +- homeassistant/components/airly/system_health.py | 2 +- 5 files changed, 25 insertions(+), 9 deletions(-) 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 From 2b7e67aa0cd5cbbf06e5631fe3463408b420e4ee Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:47:39 +0100 Subject: [PATCH 26/61] Explicitly pass in the config entry in anova coordinator init (#137701) * explicitly pass in the config_entry in anova coordinator init * fix anova --- homeassistant/components/anova/__init__.py | 5 ++-- homeassistant/components/anova/coordinator.py | 25 ++++++++++++++++--- homeassistant/components/anova/models.py | 20 --------------- homeassistant/components/anova/sensor.py | 3 +-- 4 files changed, 25 insertions(+), 28 deletions(-) delete mode 100644 homeassistant/components/anova/models.py 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) From babb1c55898450faefe1767c34533e5540a0b0d3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:51:05 +0100 Subject: [PATCH 27/61] Explicitly pass in the config_entry in bsblan coordinator init (#137725) explicitly pass in the config_entry in bsblan coordinator init --- homeassistant/components/bsblan/coordinator.py | 1 + 1 file changed, 1 insertion(+) 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(), ) From 8f1a0eadc08368dd127be1f3f47e5209d5e9f084 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:52:13 +0100 Subject: [PATCH 28/61] Pass in the config_entry in brother coordinator init (#137726) explicitly pass in the config_entry in brother coordinator init --- homeassistant/components/brother/__init__.py | 7 ++----- homeassistant/components/brother/coordinator.py | 10 +++++++++- homeassistant/components/brother/diagnostics.py | 2 +- homeassistant/components/brother/sensor.py | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) 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" From c85f7d0794f547e8f73bbc6a363d9d0235dca4f9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:52:50 +0100 Subject: [PATCH 29/61] Explicitly pass in the config_entry in blink coordinator init (#137727) explicitly pass in the config_entry in blink coordinator init --- homeassistant/components/blink/__init__.py | 2 +- homeassistant/components/blink/coordinator.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) 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), ) From 684d8dac0dc9f9209ba3d7257a0d40edd89b04ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Feb 2025 13:03:19 -0600 Subject: [PATCH 30/61] Bump SQLAlchemy to 2.0.38 (#137693) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) 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/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/package_constraints.txt b/homeassistant/package_constraints.txt index 05c05d93548..0f53b732c13 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -61,7 +61,7 @@ PyTurboJPEG==1.7.5 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 standard-aifc==3.13.0 standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 diff --git a/pyproject.toml b/pyproject.toml index c6c506dbff7..3936fdb3a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ "PyYAML==6.0.2", "requests==2.32.3", "securetar==2025.1.4", - "SQLAlchemy==2.0.37", + "SQLAlchemy==2.0.38", "standard-aifc==3.13.0", "standard-telnetlib==3.13.0", "typing-extensions>=4.12.2,<5.0", diff --git a/requirements.txt b/requirements.txt index bd54a380fd3..f0ff3b8054a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ python-slugify==8.0.4 PyYAML==6.0.2 requests==2.32.3 securetar==2025.1.4 -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 standard-aifc==3.13.0 standard-telnetlib==3.13.0 typing-extensions>=4.12.2,<5.0 diff --git a/requirements_all.txt b/requirements_all.txt index 56e7ddf173a..306e5891c86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,7 +116,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f051dde3162..efe7b9d5418 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -110,7 +110,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.37 +SQLAlchemy==2.0.38 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 From 1ff9ec661caa92072f801e9ac5e63aff56812daa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:07:01 +0100 Subject: [PATCH 31/61] Explicitly pass in the config_entry in ambient_network coordinator init (#137707) explicitly pass in the config_entry in ambient_network coordinator init --- .../components/ambient_network/__init__.py | 7 ++----- .../components/ambient_network/coordinator.py | 16 +++++++++++++--- .../components/ambient_network/sensor.py | 3 +-- 3 files changed, 16 insertions(+), 10 deletions(-) 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" From c814f4f307f33974b82939d4e1fb1a384f3ef0d9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:11:04 +0100 Subject: [PATCH 32/61] Explicitly pass in the config entry in amberelectric coordinator init (#137700) * explicitly pass in the config_entry in amberelectric coordinator init * fix amberelectric tests --- .../components/amberelectric/__init__.py | 7 +--- .../components/amberelectric/binary_sensor.py | 3 +- .../components/amberelectric/coordinator.py | 12 +++++- .../components/amberelectric/sensor.py | 3 +- .../amberelectric/test_coordinator.py | 37 +++++++++++++++---- 5 files changed, 45 insertions(+), 17 deletions(-) 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/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" From 54be256beaae4646a04ec2d2af169f3395176df7 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 7 Feb 2025 20:15:04 +0100 Subject: [PATCH 33/61] Fix wrong reference for description of password field in bring (#137720) * Fix wrong reference for description of password field in bring * Fix second occurrence as well --- homeassistant/components/bring/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index f8c261db3fd..1dbe0adbf6c 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -25,7 +25,7 @@ }, "data_description": { "email": "[%key:component::bring::config::step::user::data_description::email%]", - "password": "[%key:component::bring::config::step::user::data_description::email%]" + "password": "[%key:component::bring::config::step::user::data_description::password%]" } }, "reconfigure": { @@ -37,7 +37,7 @@ }, "data_description": { "email": "[%key:component::bring::config::step::user::data_description::email%]", - "password": "[%key:component::bring::config::step::user::data_description::email%]" + "password": "[%key:component::bring::config::step::user::data_description::password%]" } } }, From 3c7b9bec3c1ed4960fd4c819918462635c6dbe0c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:23:42 +0100 Subject: [PATCH 34/61] Explicitly pass in the config_entry in discovergy coordinator (#137734) explicitly pass in the config_entry in coordinator --- homeassistant/components/discovergy/__init__.py | 6 ++---- homeassistant/components/discovergy/coordinator.py | 7 +++++++ homeassistant/components/discovergy/diagnostics.py | 2 +- homeassistant/components/discovergy/sensor.py | 3 +-- 4 files changed, 11 insertions(+), 7 deletions(-) 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 From 55cda68866a10853cfdae0939269eabb68330edf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:34:52 +0100 Subject: [PATCH 35/61] Limit flume ConfigEntrySelect to integration domain (#137661) --- homeassistant/components/flume/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 9da0e5163c5..24ed41b21c1 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -35,7 +35,7 @@ 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}), }, ) From c300be5eeec0a7f4557b319aae9ea49f1250d710 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:41:40 +0100 Subject: [PATCH 36/61] Explicitly pass in the config_entry in aussie_broadband coordinator init (#137719) explicitly pass in the config_entry in aussie_broadband coordinator init --- homeassistant/components/aussie_broadband/__init__.py | 2 +- .../components/aussie_broadband/coordinator.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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), ) From c595b12e9829b3d91361dfaa636092c467aa59a2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:41:53 +0100 Subject: [PATCH 37/61] Explicitly pass in the config_entry in airzone coordinator init (#137702) explicitly pass in the config_entry in airzone coordinator init --- homeassistant/components/airzone/__init__.py | 7 ++----- homeassistant/components/airzone/binary_sensor.py | 3 +-- homeassistant/components/airzone/climate.py | 3 +-- homeassistant/components/airzone/coordinator.py | 13 ++++++++++++- homeassistant/components/airzone/diagnostics.py | 2 +- homeassistant/components/airzone/entity.py | 3 +-- homeassistant/components/airzone/select.py | 3 +-- homeassistant/components/airzone/sensor.py | 3 +-- homeassistant/components/airzone/switch.py | 3 +-- homeassistant/components/airzone/water_heater.py | 3 +-- 10 files changed, 22 insertions(+), 21 deletions(-) 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]] = { From 7007bd121f8ec9f8d3bf04d0b69e0a326d1c9883 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:42:05 +0100 Subject: [PATCH 38/61] Explicitly pass in the config_entry in aquacell coordinator init (#137713) explicitly pass in the config_entry in aquacell coordinator init --- homeassistant/components/aquacell/__init__.py | 9 +++------ homeassistant/components/aquacell/coordinator.py | 12 ++++++++++-- homeassistant/components/aquacell/sensor.py | 3 +-- 3 files changed, 14 insertions(+), 10 deletions(-) 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 From b9a3cab4b438fc9591fa4c617ed3c057aaa435c9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:43:54 +0100 Subject: [PATCH 39/61] Explicitly pass in the config_entry in braviatv coordinator init (#137724) explicitly pass in the config_entry in braviatv coordinator init --- homeassistant/components/braviatv/__init__.py | 7 ++----- homeassistant/components/braviatv/button.py | 3 +-- .../components/braviatv/coordinator.py | 17 +++++++++++------ .../components/braviatv/diagnostics.py | 2 +- homeassistant/components/braviatv/entity.py | 2 +- .../components/braviatv/media_player.py | 2 +- homeassistant/components/braviatv/remote.py | 2 +- 7 files changed, 18 insertions(+), 17 deletions(-) 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 From 292409f1d5d59d9fce5269953fb9654e8982bcb6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:16:09 +0100 Subject: [PATCH 40/61] Explicitly pass in the config_entry in aurora_abb_powerone coordinator init (#137715) --- .../components/aurora_abb_powerone/__init__.py | 2 +- .../aurora_abb_powerone/coordinator.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) 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. From 2d9db62828b16bde716327e980b7db6846919682 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:20:08 +0100 Subject: [PATCH 41/61] Explicitly pass in the config_entry in arve coordinator init (#137712) explicitly pass in the config_entry in arve coordinator init --- homeassistant/components/arve/__init__.py | 2 +- homeassistant/components/arve/coordinator.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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), ) From ee80966a1004b9e8b6e4462f90b33bff0ce3f2b6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:30:57 +0100 Subject: [PATCH 42/61] =?UTF-8?q?Explicitly=20pass=20in=20the=20config=5Fe?= =?UTF-8?q?ntry=20in=20android=5Fip=5Fwebcam=20coordinator=20=E2=80=A6=20(?= =?UTF-8?q?#137705)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit explicitly pass in the config_entry in android_ip_webcam coordinator init --- homeassistant/components/android_ip_webcam/coordinator.py | 1 + 1 file changed, 1 insertion(+) 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), ) From 1d3cef5c6f3d4f356cb463c335d296f6b5f6bc56 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:38:52 +0100 Subject: [PATCH 43/61] Explicitly pass in the config_entry in analytics_insight coordinator init (#137706) explicitly pass in the config_entry in analytics_insight coordinator init --- homeassistant/components/analytics_insights/__init__.py | 2 +- homeassistant/components/analytics_insights/coordinator.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) 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), ) From fd1213b70d174eccacd00a5255aec74d5639c505 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:55:41 +0100 Subject: [PATCH 44/61] Explicitly pass in the config_entry in apcupsd coordinator init (#137709) explicitly pass in the config_entry in apcupsd coordinator init --- homeassistant/components/apcupsd/__init__.py | 7 ++----- homeassistant/components/apcupsd/binary_sensor.py | 3 +-- homeassistant/components/apcupsd/coordinator.py | 13 +++++++++++-- homeassistant/components/apcupsd/diagnostics.py | 2 +- homeassistant/components/apcupsd/sensor.py | 3 +-- 5 files changed, 16 insertions(+), 12 deletions(-) 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 From 07fdec76e11a82675b924b37e602420a68608a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Fri, 7 Feb 2025 23:56:48 +0100 Subject: [PATCH 45/61] Explicitly pass in the config_entry in letpot coordinator (#137759) --- homeassistant/components/letpot/__init__.py | 7 ++----- homeassistant/components/letpot/coordinator.py | 14 +++++++++----- homeassistant/components/letpot/switch.py | 3 +-- homeassistant/components/letpot/time.py | 3 +-- 4 files changed, 13 insertions(+), 14 deletions(-) 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 From 0cbef18b73b503b3e30e52e88d934d23b86a32fe Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Feb 2025 01:01:14 +0100 Subject: [PATCH 46/61] Explicitly pass in the config_entry in eheimdigital coordinator (#137738) --- homeassistant/components/eheimdigital/__init__.py | 7 ++----- homeassistant/components/eheimdigital/climate.py | 3 +-- .../components/eheimdigital/coordinator.py | 14 +++++++++++--- homeassistant/components/eheimdigital/light.py | 3 +-- 4 files changed, 15 insertions(+), 12 deletions(-) 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) From 7fc92e4c25cec0f4e730137de426134817645f2d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Feb 2025 01:02:49 +0100 Subject: [PATCH 47/61] Explicitly pass in the config_entry in dremel_3d_printer coordinator (#137740) --- homeassistant/components/dremel_3d_printer/__init__.py | 2 +- homeassistant/components/dremel_3d_printer/coordinator.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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), ) From e9bfb6baeecfc910239e35c8cfcb4de9b9da0f6b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Feb 2025 01:04:05 +0100 Subject: [PATCH 48/61] Explicitly pass in the config_entry in emoncms coordinator (#137743) --- homeassistant/components/emoncms/__init__.py | 11 ++++------- homeassistant/components/emoncms/coordinator.py | 7 +++++++ homeassistant/components/emoncms/sensor.py | 6 +++--- 3 files changed, 14 insertions(+), 10 deletions(-) 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.""" From 61d1b34cefda6f6e1f23f583ad31f888d414511a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Feb 2025 01:04:45 +0100 Subject: [PATCH 49/61] Explicitly pass in the config_entry in dwd weather warnings coordinator (#137737) --- .../components/dwd_weather_warnings/__init__.py | 2 +- .../components/dwd_weather_warnings/coordinator.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 From 7883106e7ce9632a4ae962692cbff15b20fe0472 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Feb 2025 21:25:21 -0500 Subject: [PATCH 50/61] Make sure we always have agent_id in ConversationInput (#137679) * Make sure we always have agent_id in ConversationInput * fix type --- homeassistant/components/conversation/agent_manager.py | 3 +++ homeassistant/components/conversation/default_agent.py | 2 +- homeassistant/components/conversation/http.py | 2 +- homeassistant/components/conversation/models.py | 2 +- .../google_generative_ai_conversation/conversation.py | 2 -- .../components/openai_conversation/conversation.py | 1 - tests/components/conversation/test_init.py | 3 +++ tests/components/conversation/test_trigger.py | 10 +++++----- 8 files changed, 14 insertions(+), 11 deletions(-) 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/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/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/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/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, From aa19207ea4a12abc592781baeff674027ece33dd Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 7 Feb 2025 19:41:09 -0800 Subject: [PATCH 51/61] Clear statistics when you unload the Opower integration (#135908) * Clear statistics when you remove the Opower integration * fix --- homeassistant/components/opower/__init__.py | 6 +----- homeassistant/components/opower/coordinator.py | 14 ++++++++++++++ homeassistant/components/opower/sensor.py | 3 +-- 3 files changed, 16 insertions(+), 7 deletions(-) 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) From f64b494282e416cfafc4d2c27cd3ff4d242e2569 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Feb 2025 01:06:16 -0500 Subject: [PATCH 52/61] Conversation chat log cleanup and optimization (#137784) --- .../components/assist_pipeline/pipeline.py | 52 ++++++------ .../components/conversation/chat_log.py | 79 +++++++++++++------ .../components/conversation/test_chat_log.py | 62 +++++++++++---- 3 files changed, 130 insertions(+), 63 deletions(-) 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/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/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( From 0e0129968bef32f35ccd1ad1bca55b22d4543bec Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 8 Feb 2025 07:38:49 +0100 Subject: [PATCH 53/61] Bump onedrive_personal_sdk to 0.0.9 (#137729) --- homeassistant/components/onedrive/__init__.py | 2 +- homeassistant/components/onedrive/backup.py | 2 +- homeassistant/components/onedrive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/onedrive/__init__.py b/homeassistant/components/onedrive/__init__.py index 9716f692ec8..8355cddb0b5 100644 --- a/homeassistant/components/onedrive/__init__.py +++ b/homeassistant/components/onedrive/__init__.py @@ -133,7 +133,7 @@ async def _migrate_backup_files(client: OneDriveClient, backup_folder_id: str) - metadata_file = await client.upload_file( backup_folder_id, metadata_filename, - dumps(metadata), # type: ignore[arg-type] + dumps(metadata), ) metadata_description = { "metadata_version": 2, diff --git a/homeassistant/components/onedrive/backup.py b/homeassistant/components/onedrive/backup.py index 182e29aa63f..9926bd9cbc7 100644 --- a/homeassistant/components/onedrive/backup.py +++ b/homeassistant/components/onedrive/backup.py @@ -168,7 +168,7 @@ class OneDriveBackupAgent(BackupAgent): metadata_file = await self._client.upload_file( self._folder_id, metadata_filename, - description, # type: ignore[arg-type] + description, ) # add metadata to the metadata file 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/requirements_all.txt b/requirements_all.txt index 306e5891c86..fe7ea7fc5ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,7 +1559,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.8 +onedrive-personal-sdk==0.0.9 # homeassistant.components.onvif onvif-zeep-async==3.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efe7b9d5418..a686bbd0633 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 6c74824ac11d7fe53620fd8ab39cc8b9e9e0a7d5 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 8 Feb 2025 08:51:33 +0100 Subject: [PATCH 54/61] Add discovery for Nanoleaf Blocks and 4D (#137792) --- homeassistant/components/nanoleaf/manifest.json | 8 +++++++- homeassistant/generated/ssdp.py | 6 ++++++ homeassistant/generated/zeroconf.py | 8 ++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) 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/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", From 5f5ad34176a70569274618e028679b100ec93687 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 7 Feb 2025 23:59:20 -0800 Subject: [PATCH 55/61] Info log when Android TV Remote is unavailable (#137794) --- .../components/androidtv_remote/__init__.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) 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) From 64886f717d3a7e7737cf5b0ca497996fc2ab9aae Mon Sep 17 00:00:00 2001 From: RJPoelstra <36924801+RJPoelstra@users.noreply.github.com> Date: Sat, 8 Feb 2025 08:59:47 +0100 Subject: [PATCH 56/61] Add quality_scale to motionmount (#137012) * Mark as ready for Bronze * Add rest of quality scale rules * Evaluate all quality scale rules * Exempt repair-issues * Update dynamic-devices comment Co-authored-by: Josef Zweck --------- Co-authored-by: Josef Zweck --- .../components/motionmount/manifest.json | 1 + .../components/motionmount/quality_scale.yaml | 78 +++++++++++++++++++ script/hassfest/quality_scale.py | 2 - 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/motionmount/quality_scale.yaml 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/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", From 332a0c5082a2e6bee7aea52f285dd47910c471ba Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Sat, 8 Feb 2025 03:14:00 -0500 Subject: [PATCH 57/61] LaCrosse View new endpoint (#137284) * Switch to new endpoint in LaCrosse View * Coverage * Avoid merge conflict * Switch to UpdateFailed --- .../components/lacrosse_view/coordinator.py | 37 ++++++---- .../components/lacrosse_view/sensor.py | 2 +- tests/components/lacrosse_view/__init__.py | 67 ++++++++++++++++--- .../snapshots/test_diagnostics.ambr | 2 +- .../lacrosse_view/test_diagnostics.py | 7 +- tests/components/lacrosse_view/test_init.py | 31 ++++++--- tests/components/lacrosse_view/test_sensor.py | 50 +++++++++++--- 7 files changed, 151 insertions(+), 45 deletions(-) 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/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() From 6cb020103183834d6221a7eb8650b86c429a6e6b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 8 Feb 2025 09:57:00 +0100 Subject: [PATCH 58/61] Limit google_sheets ConfigEntrySelect to integration domain (#137766) --- homeassistant/components/google_sheets/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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]), }, From ae55e2654607c5da51cb17654e0b20ab033a4c9b Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Sat, 8 Feb 2025 10:07:22 +0100 Subject: [PATCH 59/61] Group helpers of set_up_integrations in bootstrap (#137673) --- homeassistant/bootstrap.py | 212 ++++++++++++++++++------------------- tests/test_bootstrap.py | 10 +- 2 files changed, 111 insertions(+), 111 deletions(-) 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/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" From c370fa0489557caad51ef4b02bae7a54552eb4a5 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 8 Feb 2025 01:16:10 -0800 Subject: [PATCH 60/61] Use the external URL set in Settings > System > Network if my is disabled as redirect URL for Google Drive instructions (#137791) * Use the Assistant URL set in Settings > System > Network if my is disabled * fix * Remove async_get_redirect_uri --- .../google_drive/application_credentials.py | 12 +++++-- .../test_application_credentials.py | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 tests/components/google_drive/test_application_credentials.py 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/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, + } From 327811c89aad4b9af0baedddc8b05ed3f79a9b78 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Feb 2025 10:53:13 +0100 Subject: [PATCH 61/61] Explicitly pass in the config_entry in co2signal coordinator (#137732) explicitly pass in the config_entry in coordinator --- homeassistant/components/co2signal/__init__.py | 7 ++----- .../components/co2signal/coordinator.py | 17 ++++++++++++++--- .../components/co2signal/diagnostics.py | 2 +- homeassistant/components/co2signal/sensor.py | 3 +-- 4 files changed, 18 insertions(+), 11 deletions(-) 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)