diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 7eff68703a561..1814f95d5a155 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -200,7 +200,7 @@ async def async_remove_config_entry_device( hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove lookin config entry from a device.""" - data: LookinData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data all_identifiers: set[tuple[str, str]] = { (DOMAIN, data.lookin_device.id), *((DOMAIN, remote["UUID"]) for remote in data.devices), diff --git a/homeassistant/components/medcom_ble/__init__.py b/homeassistant/components/medcom_ble/__init__.py index 8603e1b9ce567..5c508688b5442 100644 --- a/homeassistant/components/medcom_ble/__init__.py +++ b/homeassistant/components/medcom_ble/__init__.py @@ -2,34 +2,23 @@ from __future__ import annotations -from datetime import timedelta -import logging - -from bleak import BleakError -from medcom_ble import MedcomBleDeviceData - from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.unit_system import METRIC_SYSTEM -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DOMAIN +from .coordinator import MedcomBleUpdateCoordinator # Supported platforms PLATFORMS: list[Platform] = [Platform.SENSOR] -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Medcom BLE radiation monitor from a config entry.""" address = entry.unique_id - elevation = hass.config.elevation - is_metric = hass.config.units is METRIC_SYSTEM assert address is not None ble_device = bluetooth.async_ble_device_from_address(hass, address) @@ -38,26 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find Medcom BLE device with address {address}" ) - async def _async_update_method(): - """Get data from Medcom BLE radiation monitor.""" - ble_device = bluetooth.async_ble_device_from_address(hass, address) - inspector = MedcomBleDeviceData(_LOGGER, elevation, is_metric) - - try: - data = await inspector.update_device(ble_device) - except BleakError as err: - raise UpdateFailed(f"Unable to fetch data: {err}") from err - - return data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name=DOMAIN, - update_method=_async_update_method, - update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), - ) + coordinator = MedcomBleUpdateCoordinator(hass, entry, address) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/medcom_ble/coordinator.py b/homeassistant/components/medcom_ble/coordinator.py new file mode 100644 index 0000000000000..2b326c4196d52 --- /dev/null +++ b/homeassistant/components/medcom_ble/coordinator.py @@ -0,0 +1,50 @@ +"""The Medcom BLE integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from bleak import BleakError +from medcom_ble import MedcomBleDevice, MedcomBleDeviceData + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]): + """Coordinator for Medcom BLE radiation monitor data.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._address = address + self._elevation = hass.config.elevation + self._is_metric = hass.config.units is METRIC_SYSTEM + + async def _async_update_data(self) -> MedcomBleDevice: + """Get data from Medcom BLE radiation monitor.""" + ble_device = bluetooth.async_ble_device_from_address(self.hass, self._address) + inspector = MedcomBleDeviceData(_LOGGER, self._elevation, self._is_metric) + + try: + data = await inspector.update_device(ble_device) + except BleakError as err: + raise UpdateFailed(f"Unable to fetch data: {err}") from err + + return data diff --git a/homeassistant/components/medcom_ble/sensor.py b/homeassistant/components/medcom_ble/sensor.py index f837620c829e9..cf78b5dc41aab 100644 --- a/homeassistant/components/medcom_ble/sensor.py +++ b/homeassistant/components/medcom_ble/sensor.py @@ -4,8 +4,6 @@ import logging -from medcom_ble import MedcomBleDevice - from homeassistant import config_entries from homeassistant.components.sensor import ( SensorEntity, @@ -15,12 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, UNIT_CPM +from .coordinator import MedcomBleUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -41,9 +37,7 @@ async def async_setup_entry( ) -> None: """Set up Medcom BLE radiation monitor sensors.""" - coordinator: DataUpdateCoordinator[MedcomBleDevice] = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [] _LOGGER.debug("got sensors: %s", coordinator.data.sensors) @@ -62,16 +56,14 @@ async def async_setup_entry( async_add_entities(entities) -class MedcomSensor( - CoordinatorEntity[DataUpdateCoordinator[MedcomBleDevice]], SensorEntity -): +class MedcomSensor(CoordinatorEntity[MedcomBleUpdateCoordinator], SensorEntity): """Medcom BLE radiation monitor sensors for the device.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator[MedcomBleDevice], + coordinator: MedcomBleUpdateCoordinator, entity_description: SensorEntityDescription, ) -> None: """Populate the medcom entity with relevant data.""" diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index e1e9a4feb4bc5..efd7c6670d257 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -30,6 +30,7 @@ DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, + MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX, ) @@ -78,7 +79,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media_source component.""" - hass.data[DOMAIN] = {} + hass.data[MEDIA_SOURCE_DATA] = {} websocket_api.async_register_command(hass, websocket_browse_media) websocket_api.async_register_command(hass, websocket_resolve_media) frontend.async_register_built_in_panel( @@ -97,7 +98,7 @@ async def _process_media_source_platform( platform: MediaSourceProtocol, ) -> None: """Process a media source platform.""" - hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass) + hass.data[MEDIA_SOURCE_DATA][domain] = await platform.async_get_media_source(hass) @callback @@ -109,10 +110,10 @@ def _get_media_item( item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player) else: # We default to our own domain if its only one registered - domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN + domain = None if len(hass.data[MEDIA_SOURCE_DATA]) > 1 else DOMAIN return MediaSourceItem(hass, domain, "", target_media_player) - if item.domain is not None and item.domain not in hass.data[DOMAIN]: + if item.domain is not None and item.domain not in hass.data[MEDIA_SOURCE_DATA]: raise UnknownMediaSource( translation_domain=DOMAIN, translation_key="unknown_media_source", diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index 809e0d8a1fdde..38c75f19b22a3 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,10 +1,18 @@ """Constants for the media_source integration.""" +from __future__ import annotations + import re +from typing import TYPE_CHECKING from homeassistant.components.media_player import MediaClass +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .models import MediaSource DOMAIN = "media_source" +MEDIA_SOURCE_DATA: HassKey[dict[str, MediaSource]] = HassKey(DOMAIN) MEDIA_MIME_TYPES = ("audio", "video", "image") MEDIA_CLASS_MAP = { "audio": MediaClass.MUSIC, diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 4e3d6ff59db8e..c9b81e6534e65 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -6,7 +6,7 @@ import mimetypes from pathlib import Path import shutil -from typing import Any +from typing import Any, cast from aiohttp import web from aiohttp.web_request import FileField @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path -from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES +from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, MEDIA_SOURCE_DATA from .error import Unresolvable from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -30,7 +30,7 @@ def async_setup(hass: HomeAssistant) -> None: """Set up local media source.""" source = LocalSource(hass) - hass.data[DOMAIN][DOMAIN] = source + hass.data[MEDIA_SOURCE_DATA][DOMAIN] = source hass.http.register_view(LocalMediaView(hass, source)) hass.http.register_view(UploadMediaView(hass, source)) websocket_api.async_register_command(hass, websocket_remove_media) @@ -352,7 +352,7 @@ async def websocket_remove_media( connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) return - source: LocalSource = hass.data[DOMAIN][DOMAIN] + source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][DOMAIN]) try: source_dir_id, location = source.async_parse_identifier(item) diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 53bd821326215..5e64dc867f299 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, cast +from typing import TYPE_CHECKING, Any from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX +from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX @dataclass(slots=True) @@ -70,7 +70,7 @@ async def async_browse(self) -> BrowseMediaSource: can_play=False, can_expand=True, ) - for source in self.hass.data[DOMAIN].values() + for source in self.hass.data[MEDIA_SOURCE_DATA].values() ), key=lambda item: item.title, ) @@ -85,7 +85,9 @@ async def async_resolve(self) -> PlayMedia: @callback def async_media_source(self) -> MediaSource: """Return media source that owns this item.""" - return cast(MediaSource, self.hass.data[DOMAIN][self.domain]) + if TYPE_CHECKING: + assert self.domain is not None + return self.hass.data[MEDIA_SOURCE_DATA][self.domain] @classmethod def from_uri( diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 30645661ff160..d78807106c150 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -27,9 +27,11 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER] +type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDevice]]] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Establish connection with MELClooud.""" + +async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool: + """Establish connection with MELCloud.""" conf = entry.data try: mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) @@ -40,20 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (TimeoutError, ClientConnectionError) as ex: raise ConfigEntryNotReady from ex - hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) + entry.runtime_data = mel_devices await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - hass.data[DOMAIN].pop(config_entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) class MelCloudDevice: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 19c333e5825d5..b5fd57c716dec 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -24,13 +24,12 @@ HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import MelCloudDevice +from . import MelCloudConfigEntry, MelCloudDevice from .const import ( ATTR_STATUS, ATTR_VANE_HORIZONTAL, @@ -38,7 +37,6 @@ ATTR_VANE_VERTICAL, ATTR_VANE_VERTICAL_POSITIONS, CONF_POSITION, - DOMAIN, SERVICE_SET_VANE_HORIZONTAL, SERVICE_SET_VANE_VERTICAL, ) @@ -77,11 +75,11 @@ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MelCloudConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up MelCloud device climate based on config_entry.""" - mel_devices = hass.data[DOMAIN][entry.entry_id] + mel_devices = entry.runtime_data entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [ AtaDeviceClimate(mel_device, mel_device.device) for mel_device in mel_devices[DEVICE_TYPE_ATA] diff --git a/homeassistant/components/melcloud/diagnostics.py b/homeassistant/components/melcloud/diagnostics.py index 31e52bf2bde61..4606b7c25e521 100644 --- a/homeassistant/components/melcloud/diagnostics.py +++ b/homeassistant/components/melcloud/diagnostics.py @@ -5,11 +5,12 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import MelCloudConfigEntry + TO_REDACT = { CONF_USERNAME, CONF_TOKEN, @@ -17,7 +18,7 @@ async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: MelCloudConfigEntry ) -> dict[str, Any]: """Return diagnostics for the config entry.""" ent_reg = er.async_get(hass) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 51a026e717af1..36800b2645d86 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -15,13 +15,11 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import MelCloudDevice -from .const import DOMAIN +from . import MelCloudConfigEntry, MelCloudDevice @dataclasses.dataclass(frozen=True, kw_only=True) @@ -105,11 +103,11 @@ class MelcloudSensorEntityDescription(SensorEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MelCloudConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up MELCloud device sensors based on config_entry.""" - mel_devices = hass.data[DOMAIN].get(entry.entry_id) + mel_devices = entry.runtime_data entities: list[MelDeviceSensor] = [ MelDeviceSensor(mel_device, description) diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 76fbad41575d3..f006df2478e76 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -17,22 +17,21 @@ WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, MelCloudDevice +from . import MelCloudConfigEntry, MelCloudDevice from .const import ATTR_STATUS async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MelCloudConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up MelCloud device climate based on config_entry.""" - mel_devices = hass.data[DOMAIN][entry.entry_id] + mel_devices = entry.runtime_data async_add_entities( [ AtwWaterHeater(mel_device, mel_device.device) diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 6ab725d747cc6..2d9faf91bd221 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -6,13 +6,11 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN -from .coordinator import MelnorDataUpdateCoordinator +from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.NUMBER, @@ -22,11 +20,8 @@ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool: """Set up melnor from a config entry.""" - - hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) - ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS]) if not ble_device: @@ -60,20 +55,14 @@ def _async_update_ble( coordinator = MelnorDataUpdateCoordinator(hass, entry, device) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool: """Unload a config entry.""" + await entry.runtime_data.data.disconnect() - device: Device = hass.data[DOMAIN][entry.entry_id].data - - await device.disconnect() - - 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/melnor/coordinator.py b/homeassistant/components/melnor/coordinator.py index 52662fd0c4cd9..a57a1816e376b 100644 --- a/homeassistant/components/melnor/coordinator.py +++ b/homeassistant/components/melnor/coordinator.py @@ -11,15 +11,17 @@ _LOGGER = logging.getLogger(__name__) +type MelnorConfigEntry = ConfigEntry[MelnorDataUpdateCoordinator] + class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]): """Melnor data update coordinator.""" - config_entry: ConfigEntry + config_entry: MelnorConfigEntry _device: Device def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device + self, hass: HomeAssistant, config_entry: MelnorConfigEntry, device: Device ) -> None: """Initialize my coordinator.""" super().__init__( diff --git a/homeassistant/components/melnor/number.py b/homeassistant/components/melnor/number.py index 42c22ae5a439f..863faf080bde0 100644 --- a/homeassistant/components/melnor/number.py +++ b/homeassistant/components/melnor/number.py @@ -13,13 +13,11 @@ NumberEntityDescription, NumberMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import MelnorDataUpdateCoordinator +from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator from .entity import MelnorZoneEntity, get_entities_for_valves @@ -67,12 +65,12 @@ class MelnorZoneNumberEntityDescription(NumberEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MelnorConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the number platform.""" - coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( get_entities_for_valves( diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index 525a29dc6cf3b..e645019f1e85d 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -15,7 +15,6 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -26,8 +25,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util -from .const import DOMAIN -from .coordinator import MelnorDataUpdateCoordinator +from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator from .entity import MelnorBluetoothEntity, MelnorZoneEntity, get_entities_for_valves @@ -104,12 +102,12 @@ class MelnorSensorEntityDescription(SensorEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MelnorConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data # Device-level sensors async_add_entities( diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index cc5abe8f6f378..d0240a471b648 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -13,12 +13,10 @@ SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import MelnorDataUpdateCoordinator +from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator from .entity import MelnorZoneEntity, get_entities_for_valves @@ -51,12 +49,12 @@ class MelnorSwitchEntityDescription(SwitchEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MelnorConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the switch platform.""" - coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( get_entities_for_valves( diff --git a/homeassistant/components/melnor/time.py b/homeassistant/components/melnor/time.py index 277eb6e36eb92..978801dd64cb7 100644 --- a/homeassistant/components/melnor/time.py +++ b/homeassistant/components/melnor/time.py @@ -10,13 +10,11 @@ from melnor_bluetooth.device import Valve from homeassistant.components.time import TimeEntity, TimeEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import MelnorDataUpdateCoordinator +from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator from .entity import MelnorZoneEntity, get_entities_for_valves @@ -41,12 +39,12 @@ class MelnorZoneTimeEntityDescription(TimeEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MelnorConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the number platform.""" - coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( get_entities_for_valves( diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 62d7d21134c02..05be513428374 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -1,59 +1,21 @@ """The met_eireann component.""" -from collections.abc import Mapping -from datetime import timedelta -import logging -from typing import Any, Self - -import meteireann - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -UPDATE_INTERVAL = timedelta(minutes=60) +from .coordinator import MetEireannUpdateCoordinator PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Met Éireann as config entry.""" - hass.data.setdefault(DOMAIN, {}) - - raw_weather_data = meteireann.WeatherData( - async_get_clientsession(hass), - latitude=config_entry.data[CONF_LATITUDE], - longitude=config_entry.data[CONF_LONGITUDE], - altitude=config_entry.data[CONF_ELEVATION], - ) - - weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data) - - async def _async_update_data() -> MetEireannWeatherData: - """Fetch data from Met Éireann.""" - try: - return await weather_data.fetch_data() - except Exception as err: - raise UpdateFailed(f"Update failed: {err}") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=config_entry, - name=DOMAIN, - update_method=_async_update_data, - update_interval=UPDATE_INTERVAL, - ) + coordinator = MetEireannUpdateCoordinator(hass, config_entry=config_entry) await coordinator.async_refresh() - hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -68,26 +30,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok - - -class MetEireannWeatherData: - """Keep data for Met Éireann weather entities.""" - - def __init__( - self, config: Mapping[str, Any], weather_data: meteireann.WeatherData - ) -> None: - """Initialise the weather entity data.""" - self._config = config - self._weather_data = weather_data - self.current_weather_data: dict[str, Any] = {} - self.daily_forecast: list[dict[str, Any]] = [] - self.hourly_forecast: list[dict[str, Any]] = [] - - async def fetch_data(self) -> Self: - """Fetch data from API - (current weather and forecast).""" - await self._weather_data.fetching_data() - self.current_weather_data = self._weather_data.get_current_weather() - time_zone = dt_util.get_default_time_zone() - self.daily_forecast = self._weather_data.get_forecast(time_zone, False) - self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) - return self diff --git a/homeassistant/components/met_eireann/coordinator.py b/homeassistant/components/met_eireann/coordinator.py new file mode 100644 index 0000000000000..fb8c85f6b8d34 --- /dev/null +++ b/homeassistant/components/met_eireann/coordinator.py @@ -0,0 +1,76 @@ +"""The met_eireann component.""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import timedelta +import logging +from typing import Any, Self + +import meteireann + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(minutes=60) + + +class MetEireannWeatherData: + """Keep data for Met Éireann weather entities.""" + + def __init__( + self, config: Mapping[str, Any], weather_data: meteireann.WeatherData + ) -> None: + """Initialise the weather entity data.""" + self._config = config + self._weather_data = weather_data + self.current_weather_data: dict[str, Any] = {} + self.daily_forecast: list[dict[str, Any]] = [] + self.hourly_forecast: list[dict[str, Any]] = [] + + async def fetch_data(self) -> Self: + """Fetch data from API - (current weather and forecast).""" + await self._weather_data.fetching_data() + self.current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.get_default_time_zone() + self.daily_forecast = self._weather_data.get_forecast(time_zone, False) + self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) + return self + + +class MetEireannUpdateCoordinator(DataUpdateCoordinator[MetEireannWeatherData]): + """Coordinator for Met Éireann weather data.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + raw_weather_data = meteireann.WeatherData( + async_get_clientsession(hass), + latitude=config_entry.data[CONF_LATITUDE], + longitude=config_entry.data[CONF_LONGITUDE], + altitude=config_entry.data[CONF_ELEVATION], + ) + self._weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data) + + async def _async_update_data(self) -> MetEireannWeatherData: + """Fetch data from Met Éireann.""" + try: + return await self._weather_data.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 97bbd952740e3..68f46f0a65645 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -1,7 +1,6 @@ """Support for Met Éireann weather service.""" from collections.abc import Mapping -import logging from typing import Any, cast from homeassistant.components.weather import ( @@ -29,10 +28,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util -from . import MetEireannWeatherData from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP - -_LOGGER = logging.getLogger(__name__) +from .coordinator import MetEireannWeatherData def format_condition(condition: str | None) -> str | None: diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 5f1d52695383a..20e6c02f5d405 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -23,7 +23,6 @@ COORDINATOR_RAIN, DOMAIN, PLATFORMS, - UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -130,10 +129,9 @@ async def _async_update_data_alert() -> CurrentPhenomenons: entry.title, ) - undo_listener = entry.add_update_listener(_async_update_listener) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) hass.data[DOMAIN][entry.entry_id] = { - UNDO_UPDATE_LISTENER: undo_listener, COORDINATOR_FORECAST: coordinator_forecast, } if coordinator_rain and coordinator_rain.last_update_success: @@ -163,7 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 382a56d50d707..cde2812b0595f 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -26,7 +26,6 @@ COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" -UNDO_UPDATE_LISTENER = "undo_update_listener" ATTRIBUTION = "Data provided by Météo-France" MODEL = "Météo-France mobile API" MANUFACTURER = "Météo-France" diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 8c2fb41c634c3..99f72fe726baa 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -1,43 +1,15 @@ """Support for Meteoclimatic weather data.""" -import logging - -from meteoclimatic import MeteoclimaticClient -from meteoclimatic.exceptions import MeteoclimaticError - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORMS +from .coordinator import MeteoclimaticUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Meteoclimatic entry.""" - station_code = entry.data[CONF_STATION_CODE] - meteoclimatic_client = MeteoclimaticClient() - - async def async_update_data(): - """Obtain the latest data from Meteoclimatic.""" - try: - data = await hass.async_add_executor_job( - meteoclimatic_client.weather_at_station, station_code - ) - except MeteoclimaticError as err: - raise UpdateFailed(f"Error while retrieving data: {err}") from err - return data.__dict__ - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name=f"Meteoclimatic weather for {entry.title} ({station_code})", - update_method=async_update_data, - update_interval=SCAN_INTERVAL, - ) - + coordinator = MeteoclimaticUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/meteoclimatic/coordinator.py b/homeassistant/components/meteoclimatic/coordinator.py new file mode 100644 index 0000000000000..2e9264dd3ef4d --- /dev/null +++ b/homeassistant/components/meteoclimatic/coordinator.py @@ -0,0 +1,43 @@ +"""Support for Meteoclimatic weather data.""" + +import logging +from typing import Any + +from meteoclimatic import MeteoclimaticClient +from meteoclimatic.exceptions import MeteoclimaticError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_STATION_CODE, SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator for Meteoclimatic weather data.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + self._station_code = entry.data[CONF_STATION_CODE] + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=f"Meteoclimatic weather for {entry.title} ({self._station_code})", + update_interval=SCAN_INTERVAL, + ) + self._meteoclimatic_client = MeteoclimaticClient() + + async def _async_update_data(self) -> dict[str, Any]: + """Obtain the latest data from Meteoclimatic.""" + try: + data = await self.hass.async_add_executor_job( + self._meteoclimatic_client.weather_at_station, self._station_code + ) + except MeteoclimaticError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") from err + return data.__dict__ diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 6e508bd63d834..2d80ccda30cd8 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -18,12 +18,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL +from .coordinator import MeteoclimaticUpdateCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -119,7 +117,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Meteoclimatic sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES], @@ -127,13 +125,17 @@ async def async_setup_entry( ) -class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): +class MeteoclimaticSensor( + CoordinatorEntity[MeteoclimaticUpdateCoordinator], SensorEntity +): """Representation of a Meteoclimatic sensor.""" _attr_attribution = ATTRIBUTION def __init__( - self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription + self, + coordinator: MeteoclimaticUpdateCoordinator, + description: SensorEntityDescription, ) -> None: """Initialize the Meteoclimatic sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index fa3b3c92288cb..ba74cfeca5e28 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -8,12 +8,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL +from .coordinator import MeteoclimaticUpdateCoordinator def format_condition(condition): @@ -31,12 +29,14 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Meteoclimatic weather platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([MeteoclimaticWeather(coordinator)], False) -class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): +class MeteoclimaticWeather( + CoordinatorEntity[MeteoclimaticUpdateCoordinator], WeatherEntity +): """Representation of a weather condition.""" _attr_attribution = ATTRIBUTION @@ -44,7 +44,7 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - def __init__(self, coordinator: DataUpdateCoordinator) -> None: + def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) self._unique_id = self.coordinator.data["station"].code diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index c7683ebedd6ef..6e5c4c6181f3d 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -10,13 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import ( - CONF_NOT_FIRST_RUN, - DOMAIN, - FIRST_RUN, - MONOPRICE_OBJECT, - UNDO_UPDATE_LISTENER, -) +from .const import CONF_NOT_FIRST_RUN, DOMAIN, FIRST_RUN, MONOPRICE_OBJECT PLATFORMS = [Platform.MEDIA_PLAYER] @@ -41,11 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, CONF_NOT_FIRST_RUN: True} ) - undo_listener = entry.add_update_listener(_update_listener) + entry.async_on_unload(entry.add_update_listener(_update_listener)) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { MONOPRICE_OBJECT: monoprice, - UNDO_UPDATE_LISTENER: undo_listener, FIRST_RUN: first_run, } @@ -60,8 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not unload_ok: return False - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - def _cleanup(monoprice) -> None: """Destroy the Monoprice object. diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py index 576e4aa0e6909..9dc9cad38319f 100644 --- a/homeassistant/components/monoprice/const.py +++ b/homeassistant/components/monoprice/const.py @@ -18,4 +18,3 @@ FIRST_RUN = "first_run" MONOPRICE_OBJECT = "monoprice_object" -UNDO_UPDATE_LISTENER = "update_update_listener" diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index e29491e2b219a..4b28a1029a4bd 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/music_assistant", "iot_class": "local_push", "loggers": ["music_assistant"], - "requirements": ["music-assistant-client==1.2.3"], + "requirements": ["music-assistant-client==1.2.4"], "zeroconf": ["_mass._tcp.local."] } diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index b748aad241c29..3a21085639108 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -248,8 +248,6 @@ async def async_on_update(self) -> None: player = self.player active_queue = self.active_queue # update generic attributes - if player.powered and active_queue is not None: - self._attr_state = MediaPlayerState(active_queue.state.value) if player.powered and player.playback_state is not None: self._attr_state = MediaPlayerState(player.playback_state.value) else: diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 09cd7b42da019..9094fc11e1ca3 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -9,13 +9,11 @@ from pymystrom.exceptions import MyStromConnectionError from pymystrom.switch import MyStromSwitch -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN -from .models import MyStromData +from .models import MyStromConfigEntry, MyStromData PLATFORMS_PLUGS = [Platform.SENSOR, Platform.SWITCH] PLATFORMS_BULB = [Platform.LIGHT] @@ -41,7 +39,7 @@ def _get_mystrom_switch(host: str) -> MyStromSwitch: return MyStromSwitch(host) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool: """Set up myStrom from a config entry.""" host = entry.data[CONF_HOST] try: @@ -73,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unsupported myStrom device type: %s", device_type) return False - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData( + entry.runtime_data = MyStromData( device=device, info=info, ) @@ -82,15 +80,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool: """Unload a config entry.""" - device_type = hass.data[DOMAIN][entry.entry_id].info["type"] + device_type = entry.runtime_data.info["type"] platforms = [] if device_type in [101, 106, 107, 120]: platforms.extend(PLATFORMS_PLUGS) elif device_type in [102, 105]: platforms.extend(PLATFORMS_BULB) - 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/mystrom/light.py b/homeassistant/components/mystrom/light.py index 3942f601a2093..67964d7d5b40e 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -15,12 +15,12 @@ LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, MANUFACTURER +from .models import MyStromConfigEntry _LOGGER = logging.getLogger(__name__) @@ -32,12 +32,12 @@ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MyStromConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the myStrom entities.""" - info = hass.data[DOMAIN][entry.entry_id].info - device = hass.data[DOMAIN][entry.entry_id].device + info = entry.runtime_data.info + device = entry.runtime_data.device async_add_entities([MyStromLight(device, entry.title, info["mac"])]) diff --git a/homeassistant/components/mystrom/models.py b/homeassistant/components/mystrom/models.py index 694a2f43df6bf..a96837070fd25 100644 --- a/homeassistant/components/mystrom/models.py +++ b/homeassistant/components/mystrom/models.py @@ -6,6 +6,10 @@ from pymystrom.bulb import MyStromBulb from pymystrom.switch import MyStromSwitch +from homeassistant.config_entries import ConfigEntry + +type MyStromConfigEntry = ConfigEntry[MyStromData] + @dataclass class MyStromData: diff --git a/homeassistant/components/mystrom/sensor.py b/homeassistant/components/mystrom/sensor.py index bd5c9b923a20b..251765d1658b1 100644 --- a/homeassistant/components/mystrom/sensor.py +++ b/homeassistant/components/mystrom/sensor.py @@ -13,13 +13,13 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPower, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, MANUFACTURER +from .models import MyStromConfigEntry @dataclass(frozen=True) @@ -56,11 +56,11 @@ class MyStromSwitchSensorEntityDescription(SensorEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MyStromConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the myStrom entities.""" - device: MyStromSwitch = hass.data[DOMAIN][entry.entry_id].device + device: MyStromSwitch = entry.runtime_data.device async_add_entities( MyStromSwitchSensor(device, entry.title, description) diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index f626656a4e3d2..860d2dff727d8 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -8,12 +8,12 @@ from pymystrom.exceptions import MyStromConnectionError from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, MANUFACTURER +from .models import MyStromConfigEntry DEFAULT_NAME = "myStrom Switch" @@ -22,11 +22,11 @@ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MyStromConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the myStrom entities.""" - device = hass.data[DOMAIN][entry.entry_id].device + device = entry.runtime_data.device async_add_entities([MyStromSwitch(device, entry.title)]) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index dbde1ee1ef358..e2b9f8631d838 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -124,6 +124,7 @@ def __init__(self) -> None: self._model: str | None = None self._connect_result: str | None = None self._method: str | None = None + self._port: int | None = None self._name: str | None = None self._title: str = "" self._id: int | None = None @@ -199,33 +200,37 @@ def _async_update_and_abort_for_matching_unique_id(self) -> None: async def _async_create_bridge(self) -> None: """Create the bridge.""" - result, method, _info = await self._async_get_device_info_and_method() + result = await self._async_load_device_info() if result not in SUCCESSFUL_RESULTS: LOGGER.debug("No working config found for %s", self._host) raise AbortFlow(result) - assert method is not None - self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) + assert self._method is not None + self._bridge = SamsungTVBridge.get_bridge( + self.hass, self._method, self._host, self._port + ) - async def _async_get_device_info_and_method( + async def _async_load_device_info( self, - ) -> tuple[str, str | None, dict[str, Any] | None]: + ) -> str: """Get device info and method only once.""" if self._connect_result is None: - result, _, method, info = await async_get_device_info(self.hass, self._host) + result, port, method, info = await async_get_device_info( + self.hass, self._host + ) self._connect_result = result self._method = method + self._port = port self._device_info = info if not method: LOGGER.debug("Host:%s did not return device info", self._host) - return result, None, None - return self._connect_result, self._method, self._device_info + return self._connect_result async def _async_get_and_check_device_info(self) -> bool: """Try to get the device info.""" - result, _method, info = await self._async_get_device_info_and_method() + result = await self._async_load_device_info() if result not in SUCCESSFUL_RESULTS: raise AbortFlow(result) - if not info: + if not (info := self._device_info): return False dev_info = info.get("device", {}) assert dev_info is not None diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 7445182e5f6be..5029e8c610842 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -548,8 +548,8 @@ "description": "Sets the configuration for a lock.", "fields": { "auto_relock_time": { - "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`.", - "name": "Auto relock time" + "description": "Duration in seconds until lock returns to locked state. Only enforced when operation type is `constant`.", + "name": "Autorelock time" }, "block_to_block": { "description": "Whether the lock should run the motor until it hits resistance.", diff --git a/requirements_all.txt b/requirements_all.txt index 319da5326631d..7e2ca3412637b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1467,7 +1467,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.2.3 +music-assistant-client==1.2.4 # homeassistant.components.tts mutagen==1.47.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 324667af4dd9b..89ec74a587c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1259,7 +1259,7 @@ mozart-api==4.1.1.116.4 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.2.3 +music-assistant-client==1.2.4 # homeassistant.components.tts mutagen==1.47.0 diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 4eefe3166b5c4..26683ced66e34 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -40,15 +40,9 @@ from . import KnxEntityGenerator -from tests.common import ( - MockConfigEntry, - async_load_json_object_fixture, - load_json_object_fixture, -) +from tests.common import MockConfigEntry, async_load_json_object_fixture from tests.typing import WebSocketGenerator -FIXTURE_PROJECT_DATA = load_json_object_fixture("project.json", DOMAIN) - class KNXTestKit: """Test helper for the KNX integration.""" @@ -338,11 +332,19 @@ async def knx( @pytest.fixture -def load_knxproj(hass_storage: dict[str, Any]) -> None: +async def project_data(hass: HomeAssistant) -> dict[str, Any]: + """Return the fixture project data.""" + return await async_load_json_object_fixture(hass, "project.json", DOMAIN) + + +@pytest.fixture +async def load_knxproj( + project_data: dict[str, Any], hass_storage: dict[str, Any] +) -> None: """Mock KNX project data.""" hass_storage[KNX_PROJECT_STORAGE_KEY] = { "version": 1, - "data": FIXTURE_PROJECT_DATA, + "data": project_data, } diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index ab4ecf876dca5..5c0f002a54102 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from .conftest import FIXTURE_PROJECT_DATA, KNXTestKit +from .conftest import KNXTestKit from tests.typing import WebSocketGenerator @@ -32,11 +32,11 @@ async def test_knx_info_command( assert res["result"]["project"] is None +@pytest.mark.usefixtures("load_knxproj") async def test_knx_info_command_with_project( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator, - load_knxproj: None, ) -> None: """Test knx/info command with loaded project.""" await knx.setup_integration() @@ -59,11 +59,11 @@ async def test_knx_project_file_process( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], + project_data: dict[str, Any], ) -> None: """Test knx/project_file_process command for storing and loading new data.""" _file_id = "1234" _password = "pw-test" - _parse_result = FIXTURE_PROJECT_DATA await knx.setup_integration() client = await hass_ws_client(hass) @@ -80,7 +80,7 @@ async def test_knx_project_file_process( patch( "homeassistant.components.knx.project.process_uploaded_file", ) as file_upload_mock, - patch("xknxproject.XKNXProj.parse", return_value=_parse_result) as parse_mock, + patch("xknxproject.XKNXProj.parse", return_value=project_data) as parse_mock, ): file_upload_mock.return_value.__enter__.return_value = "" res = await client.receive_json() @@ -90,7 +90,7 @@ async def test_knx_project_file_process( assert res["success"], res assert hass.data[KNX_MODULE_KEY].project.loaded - assert hass_storage[KNX_PROJECT_STORAGE_KEY]["data"] == _parse_result + assert hass_storage[KNX_PROJECT_STORAGE_KEY]["data"] == project_data async def test_knx_project_file_process_error( @@ -124,11 +124,11 @@ async def test_knx_project_file_process_error( assert not hass.data[KNX_MODULE_KEY].project.loaded +@pytest.mark.usefixtures("load_knxproj") async def test_knx_project_file_remove( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator, - load_knxproj: None, hass_storage: dict[str, Any], ) -> None: """Test knx/project_file_remove command.""" @@ -145,11 +145,12 @@ async def test_knx_project_file_remove( assert not hass_storage.get(KNX_PROJECT_STORAGE_KEY) +@pytest.mark.usefixtures("load_knxproj") async def test_knx_get_project( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator, - load_knxproj: None, + project_data: dict[str, Any], ) -> None: """Test retrieval of kxnproject from store.""" await knx.setup_integration() @@ -160,7 +161,7 @@ async def test_knx_get_project( res = await client.receive_json() assert res["success"], res assert res["result"]["project_loaded"] is True - assert res["result"]["knxproject"] == FIXTURE_PROJECT_DATA + assert res["result"]["knxproject"] == project_data async def test_knx_group_monitor_info_command( diff --git a/tests/components/met_eireann/__init__.py b/tests/components/met_eireann/__init__.py index c38f197691a98..a65ba64accde9 100644 --- a/tests/components/met_eireann/__init__.py +++ b/tests/components/met_eireann/__init__.py @@ -19,7 +19,7 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: } entry = MockConfigEntry(domain=DOMAIN, data=entry_data) with patch( - "homeassistant.components.met_eireann.meteireann.WeatherData.fetching_data", + "homeassistant.components.met_eireann.coordinator.meteireann.WeatherData.fetching_data", return_value=True, ): entry.add_to_hass(hass) diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index 1e385c9a600b2..54931dd4c1219 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -6,8 +6,8 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.met_eireann import UPDATE_INTERVAL from homeassistant.components.met_eireann.const import DOMAIN +from homeassistant.components.met_eireann.coordinator import UPDATE_INTERVAL from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, SERVICE_GET_FORECASTS, diff --git a/tests/components/meteoclimatic/conftest.py b/tests/components/meteoclimatic/conftest.py index a481b811a773e..8bd600a4f6f47 100644 --- a/tests/components/meteoclimatic/conftest.py +++ b/tests/components/meteoclimatic/conftest.py @@ -8,7 +8,9 @@ @pytest.fixture(autouse=True) def patch_requests(): """Stub out services that makes requests.""" - patch_client = patch("homeassistant.components.meteoclimatic.MeteoclimaticClient") + patch_client = patch( + "homeassistant.components.meteoclimatic.coordinator.MeteoclimaticClient" + ) with patch_client: yield diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d63e5a7ae2aeb..dd6b21ab5e546 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -161,6 +161,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id is None @@ -195,6 +196,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: assert result3["data"][CONF_METHOD] == METHOD_LEGACY assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result3["data"][CONF_MODEL] is None + assert result3["data"][CONF_PORT] == 55000 assert result3["result"].unique_id is None @@ -224,6 +226,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -272,6 +275,7 @@ async def test_user_encrypted_websocket( assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung" assert result4["data"][CONF_MODEL] == "UE48JU6400" + assert result4["data"][CONF_PORT] == 8000 assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" @@ -402,6 +406,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.20.43.21" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -464,6 +469,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -522,6 +528,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -557,6 +564,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["data"][CONF_PORT] == 55000 assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @@ -599,6 +607,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert ( result["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "http://10.10.12.34:7676/smp_15_" @@ -630,6 +639,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert ( result["data"][CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" @@ -681,6 +691,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result4["data"][CONF_MODEL] == "UE48JU6400" + assert result4["data"][CONF_PORT] == 8000 assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "http://10.10.12.34:7676/smp_15_" @@ -887,6 +898,7 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE48JU6400" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @@ -919,6 +931,7 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: assert result["data"][CONF_MAC] == "aa:ee:tt:hh:ee:rr" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE43LS003" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1020,6 +1033,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["data"][CONF_PORT] == 8002 assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1129,6 +1143,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" + assert result["data"][CONF_PORT] == 8002 remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -1180,6 +1195,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" + assert result["data"][CONF_PORT] == 8002 remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -2091,6 +2107,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" assert entry.data[CONF_MAC] is None + assert entry.data[CONF_PORT] == 8002 assert entry.unique_id == "123" device_info = deepcopy(MOCK_DEVICE_INFO)