diff --git a/.coveragerc b/.coveragerc index 10dedd43e81..7986785d86e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -519,6 +519,7 @@ omit = homeassistant/components/guardian/util.py homeassistant/components/guardian/valve.py homeassistant/components/habitica/__init__.py + homeassistant/components/habitica/coordinator.py homeassistant/components/habitica/sensor.py homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harmony/data.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40757c09e95..07d6c785168 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.2 + rev: v0.4.3 hooks: - id: ruff args: diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 4d248a06ac3..576b77ee0cb 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -31,7 +31,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.dt import utc_from_timestamp from . import AccuWeatherData @@ -65,8 +64,6 @@ class AccuWeatherEntity( CoordinatorWeatherEntity[ AccuWeatherObservationDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator, - TimestampDataUpdateCoordinator, - TimestampDataUpdateCoordinator, ] ): """Define an AccuWeather entity.""" diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index c64fc273a2a..dcd08cf6fc3 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -17,15 +17,18 @@ from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import DOMAIN from .helpers import create_api, get_enable_ime _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE] +AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry +) -> bool: """Set up Android TV Remote from a config entry.""" api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry)) @@ -64,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # update the config entry data and reload the config entry. api.keep_reconnecting(reauth_needed) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api + entry.runtime_data = api await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -77,17 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) entry.async_on_unload(entry.add_update_listener(update_listener)) + entry.async_on_unload(api.disconnect) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id) - api.disconnect() - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/androidtv_remote/diagnostics.py b/homeassistant/components/androidtv_remote/diagnostics.py index 757b3bd4e83..41595451be8 100644 --- a/homeassistant/components/androidtv_remote/diagnostics.py +++ b/homeassistant/components/androidtv_remote/diagnostics.py @@ -4,23 +4,20 @@ from __future__ import annotations from typing import Any -from androidtvremote2 import AndroidTVRemote - from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant -from .const import DOMAIN +from . import AndroidTVRemoteConfigEntry TO_REDACT = {CONF_HOST, CONF_MAC} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id) + api = entry.runtime_data return async_redact_data( { "api_device_info": api.device_info, diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index f45dee34afe..915586b3879 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.14"], + "requirements": ["androidtvremote2==0.0.15"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/homeassistant/components/androidtv_remote/media_player.py b/homeassistant/components/androidtv_remote/media_player.py index 997f3fb040a..571eab4a15b 100644 --- a/homeassistant/components/androidtv_remote/media_player.py +++ b/homeassistant/components/androidtv_remote/media_player.py @@ -14,12 +14,11 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AndroidTVRemoteConfigEntry from .entity import AndroidTVRemoteBaseEntity PARALLEL_UPDATES = 0 @@ -27,11 +26,11 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AndroidTVRemoteConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Android TV media player entity based on a config entry.""" - api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id] + api = config_entry.runtime_data async_add_entities([AndroidTVRemoteMediaPlayerEntity(api, config_entry)]) @@ -53,7 +52,9 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt | MediaPlayerEntityFeature.PLAY_MEDIA ) - def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None: + def __init__( + self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry + ) -> None: """Initialize the entity.""" super().__init__(api, config_entry) diff --git a/homeassistant/components/androidtv_remote/remote.py b/homeassistant/components/androidtv_remote/remote.py index 3dc5534e54f..72387a54bf0 100644 --- a/homeassistant/components/androidtv_remote/remote.py +++ b/homeassistant/components/androidtv_remote/remote.py @@ -6,8 +6,6 @@ import asyncio from collections.abc import Iterable from typing import Any -from androidtvremote2 import AndroidTVRemote - from homeassistant.components.remote import ( ATTR_ACTIVITY, ATTR_DELAY_SECS, @@ -19,11 +17,10 @@ from homeassistant.components.remote import ( RemoteEntity, RemoteEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AndroidTVRemoteConfigEntry from .entity import AndroidTVRemoteBaseEntity PARALLEL_UPDATES = 0 @@ -31,11 +28,11 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AndroidTVRemoteConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Android TV remote entity based on a config entry.""" - api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id] + api = config_entry.runtime_data async_add_entities([AndroidTVRemoteEntity(api, config_entry)]) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 3d825cd99b5..b631c61a18d 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -651,11 +651,8 @@ def websocket_delete_all_refresh_tokens( continue try: hass.auth.async_remove_refresh_token(token) - except Exception as err: # pylint: disable=broad-except - getLogger(__name__).exception( - "During refresh token removal, the following error occurred: %s", - err, - ) + except Exception: # pylint: disable=broad-except + getLogger(__name__).exception("Error during refresh token removal") remove_failed = True if remove_failed: diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index f955ff398d7..8f197d8924d 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -13,8 +13,10 @@ from .hub import AxisHub, get_axis_api _LOGGER = logging.getLogger(__name__) +AxisConfigEntry = ConfigEntry[AxisHub] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry) -> bool: """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) @@ -25,8 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - hub = AxisHub(hass, config_entry, api) - hass.data[AXIS_DOMAIN][config_entry.entry_id] = hub + hub = config_entry.runtime_data = AxisHub(hass, config_entry, api) await hub.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) hub.setup() @@ -42,7 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Axis device config entry.""" - hass.data[AXIS_DOMAIN].pop(config_entry.entry_id) return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 8cd90ba1554..d6f132874b6 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -17,11 +17,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from . import AxisConfigEntry from .entity import AxisEventDescription, AxisEventEntity from .hub import AxisHub @@ -177,11 +177,11 @@ ENTITY_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AxisConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + config_entry.runtime_data.entity_loader.register_platform( async_add_entities, AxisBinarySensor, ENTITY_DESCRIPTIONS ) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 025244fb675..a5a00bcd1ab 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -4,12 +4,12 @@ from urllib.parse import urlencode from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging -from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_DIGEST_AUTHENTICATION from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AxisConfigEntry from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE from .entity import AxisEntity from .hub import AxisHub @@ -17,13 +17,13 @@ from .hub import AxisHub async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AxisConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Axis camera video stream.""" filter_urllib3_logging() - hub = AxisHub.get_hub(hass, config_entry) + hub = config_entry.runtime_data if ( not (prop := hub.api.vapix.params.property_handler.get("0")) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 80872fc9be4..1754e37853f 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -32,6 +32,7 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac from homeassistant.util.network import is_link_local +from . import AxisConfigEntry from .const import ( CONF_STREAM_PROFILE, CONF_VIDEO_SOURCE, @@ -260,13 +261,14 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle Axis device options.""" + config_entry: AxisConfigEntry hub: AxisHub async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Manage the Axis device options.""" - self.hub = AxisHub.get_hub(self.hass, self.config_entry) + self.hub = self.config_entry.runtime_data return await self.async_step_configure_stream() async def async_step_configure_stream( diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index d2386047e71..ffc2b36db82 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -5,11 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .hub import AxisHub +from . import AxisConfigEntry REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"} @@ -17,10 +16,10 @@ REDACT_VAPIX_PARAMS = {"root.Network", "System.SerialNumber"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: AxisConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - hub = AxisHub.get_hub(hass, config_entry) + hub = config_entry.runtime_data diag: dict[str, Any] = hub.additional_diagnostics.copy() diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py index 4e58e3be7c6..9dd4280f833 100644 --- a/homeassistant/components/axis/hub/hub.py +++ b/homeassistant/components/axis/hub/hub.py @@ -2,11 +2,10 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import axis -from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac @@ -17,12 +16,15 @@ from .config import AxisConfig from .entity_loader import AxisEntityLoader from .event_source import AxisEventSource +if TYPE_CHECKING: + from .. import AxisConfigEntry + class AxisHub: """Manages a Axis device.""" def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice + self, hass: HomeAssistant, config_entry: AxisConfigEntry, api: axis.AxisDevice ) -> None: """Initialize the device.""" self.hass = hass @@ -37,13 +39,6 @@ class AxisHub: self.additional_diagnostics: dict[str, Any] = {} - @callback - @staticmethod - def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> AxisHub: - """Get Axis hub from config entry.""" - hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id] - return hub - @property def available(self) -> bool: """Connection state to the device.""" @@ -63,7 +58,7 @@ class AxisHub: @staticmethod async def async_new_address_callback( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: AxisConfigEntry ) -> None: """Handle signals of device getting new address. @@ -71,7 +66,7 @@ class AxisHub: This is a static method because a class method (bound method), cannot be used with weak references. """ - hub = AxisHub.get_hub(hass, config_entry) + hub = config_entry.runtime_data hub.config = AxisConfig.from_config_entry(config_entry) hub.event_source.config_entry = config_entry hub.api.config.host = hub.config.host diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index af188469a74..d0d144a28fa 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -11,10 +11,10 @@ from homeassistant.components.light import ( LightEntity, LightEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AxisConfigEntry from .entity import TOPIC_TO_EVENT_TYPE, AxisEventDescription, AxisEventEntity from .hub import AxisHub @@ -45,11 +45,11 @@ ENTITY_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AxisConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Axis light platform.""" - AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + config_entry.runtime_data.entity_loader.register_platform( async_add_entities, AxisLight, ENTITY_DESCRIPTIONS ) diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 895e2a9fa01..17824302871 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -10,11 +10,11 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AxisConfigEntry from .entity import AxisEventDescription, AxisEventEntity from .hub import AxisHub @@ -38,11 +38,11 @@ ENTITY_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AxisConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Axis switch platform.""" - AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + config_entry.runtime_data.entity_loader.register_platform( async_add_entities, AxisSwitch, ENTITY_DESCRIPTIONS ) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 2eb07c5133f..789991cce9c 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -97,10 +97,9 @@ class HomeAssistantBluetoothManager(BluetoothManager): matched_domains = self._integration_matcher.match_domains(service_info) if self._debug: _LOGGER.debug( - "%s: %s %s match: %s", + "%s: %s match: %s", self._async_describe_source(service_info), - service_info.address, - service_info.advertisement, + service_info, matched_domains, ) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 754e8faf996..9a0c84d6beb 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,10 +16,10 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.1", + "bluetooth-adapters==0.19.2", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.8.1" + "habluetooth==3.0.1" ] } diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 14da3a4b92b..28f23cd9765 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -2,12 +2,17 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + from boschshcpy import SHCSession from boschshcpy.device import SHCDevice from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -20,341 +25,207 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import DATA_SESSION, DOMAIN from .entity import SHCEntity +@dataclass(frozen=True, kw_only=True) +class SHCSensorEntityDescription(SensorEntityDescription): + """Describes a SHC sensor.""" + + value_fn: Callable[[SHCDevice], StateType] + attributes_fn: Callable[[SHCDevice], dict[str, Any]] | None = None + + +TEMPERATURE_SENSOR = "temperature" +HUMIDITY_SENSOR = "humidity" +VALVE_TAPPET_SENSOR = "valvetappet" +PURITY_SENSOR = "purity" +AIR_QUALITY_SENSOR = "airquality" +TEMPERATURE_RATING_SENSOR = "temperature_rating" +HUMIDITY_RATING_SENSOR = "humidity_rating" +PURITY_RATING_SENSOR = "purity_rating" +POWER_SENSOR = "power" +ENERGY_SENSOR = "energy" +COMMUNICATION_QUALITY_SENSOR = "communication_quality" + +SENSOR_DESCRIPTIONS: dict[str, SHCSensorEntityDescription] = { + TEMPERATURE_SENSOR: SHCSensorEntityDescription( + key=TEMPERATURE_SENSOR, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda device: device.temperature, + ), + HUMIDITY_SENSOR: SHCSensorEntityDescription( + key=HUMIDITY_SENSOR, + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.humidity, + ), + PURITY_SENSOR: SHCSensorEntityDescription( + key=PURITY_SENSOR, + translation_key=PURITY_SENSOR, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + value_fn=lambda device: device.purity, + ), + AIR_QUALITY_SENSOR: SHCSensorEntityDescription( + key=AIR_QUALITY_SENSOR, + translation_key="air_quality", + value_fn=lambda device: device.combined_rating.name, + attributes_fn=lambda device: { + "rating_description": device.description, + }, + ), + TEMPERATURE_RATING_SENSOR: SHCSensorEntityDescription( + key=TEMPERATURE_RATING_SENSOR, + translation_key=TEMPERATURE_RATING_SENSOR, + value_fn=lambda device: device.temperature_rating.name, + ), + COMMUNICATION_QUALITY_SENSOR: SHCSensorEntityDescription( + key=COMMUNICATION_QUALITY_SENSOR, + translation_key=COMMUNICATION_QUALITY_SENSOR, + value_fn=lambda device: device.communicationquality.name, + ), + HUMIDITY_RATING_SENSOR: SHCSensorEntityDescription( + key=HUMIDITY_RATING_SENSOR, + translation_key=HUMIDITY_RATING_SENSOR, + value_fn=lambda device: device.humidity_rating.name, + ), + PURITY_RATING_SENSOR: SHCSensorEntityDescription( + key=PURITY_RATING_SENSOR, + translation_key=PURITY_RATING_SENSOR, + value_fn=lambda device: device.purity_rating.name, + ), + POWER_SENSOR: SHCSensorEntityDescription( + key=POWER_SENSOR, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + value_fn=lambda device: device.powerconsumption, + ), + ENERGY_SENSOR: SHCSensorEntityDescription( + key=ENERGY_SENSOR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value_fn=lambda device: device.energyconsumption / 1000.0, + ), + VALVE_TAPPET_SENSOR: SHCSensorEntityDescription( + key=VALVE_TAPPET_SENSOR, + translation_key=VALVE_TAPPET_SENSOR, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.position, + attributes_fn=lambda device: { + "valve_tappet_state": device.valvestate.name, + }, + ), +} + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SHC sensor platform.""" - entities: list[SensorEntity] = [] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] - for sensor in session.device_helper.thermostats: - entities.append( - TemperatureSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - ValveTappetSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities: list[SensorEntity] = [ + SHCSensor( + device, + SENSOR_DESCRIPTIONS[sensor_type], + session.information.unique_id, + config_entry.entry_id, ) + for device in session.device_helper.thermostats + for sensor_type in (TEMPERATURE_SENSOR, VALVE_TAPPET_SENSOR) + ] - for sensor in session.device_helper.wallthermostats: - entities.append( - TemperatureSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - HumiditySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities.extend( + SHCSensor( + device, + SENSOR_DESCRIPTIONS[sensor_type], + session.information.unique_id, + config_entry.entry_id, ) + for device in session.device_helper.wallthermostats + for sensor_type in (TEMPERATURE_SENSOR, HUMIDITY_SENSOR) + ) - for sensor in session.device_helper.twinguards: - entities.append( - TemperatureSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities.extend( + SHCSensor( + device, + SENSOR_DESCRIPTIONS[sensor_type], + session.information.unique_id, + config_entry.entry_id, ) - entities.append( - HumiditySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - PuritySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - AirQualitySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - TemperatureRatingSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - HumidityRatingSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - PurityRatingSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + for device in session.device_helper.twinguards + for sensor_type in ( + TEMPERATURE_SENSOR, + HUMIDITY_SENSOR, + PURITY_SENSOR, + AIR_QUALITY_SENSOR, + TEMPERATURE_RATING_SENSOR, + HUMIDITY_RATING_SENSOR, + PURITY_RATING_SENSOR, ) + ) - for sensor in ( - session.device_helper.smart_plugs + session.device_helper.light_switches_bsm - ): - entities.append( - PowerSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities.extend( + SHCSensor( + device, + SENSOR_DESCRIPTIONS[sensor_type], + session.information.unique_id, + config_entry.entry_id, ) - entities.append( - EnergySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + for device in ( + session.device_helper.smart_plugs + session.device_helper.light_switches_bsm ) + for sensor_type in (POWER_SENSOR, ENERGY_SENSOR) + ) - for sensor in session.device_helper.smart_plugs_compact: - entities.append( - PowerSensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - EnergySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) - entities.append( - CommunicationQualitySensor( - device=sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities.extend( + SHCSensor( + device, + SENSOR_DESCRIPTIONS[sensor_type], + session.information.unique_id, + config_entry.entry_id, ) + for device in session.device_helper.smart_plugs_compact + for sensor_type in (POWER_SENSOR, ENERGY_SENSOR, COMMUNICATION_QUALITY_SENSOR) + ) async_add_entities(entities) -class TemperatureSensor(SHCEntity, SensorEntity): - """Representation of an SHC temperature reporting sensor.""" +class SHCSensor(SHCEntity, SensorEntity): + """Representation of a SHC sensor.""" - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + entity_description: SHCSensorEntityDescription - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC temperature reporting sensor.""" + def __init__( + self, + device: SHCDevice, + entity_description: SHCSensorEntityDescription, + parent_id: str, + entry_id: str, + ) -> None: + """Initialize sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_temperature" + self.entity_description = entity_description + self._attr_unique_id = f"{device.serial}_{entity_description.key}" @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._device.temperature - - -class HumiditySensor(SHCEntity, SensorEntity): - """Representation of an SHC humidity reporting sensor.""" - - _attr_device_class = SensorDeviceClass.HUMIDITY - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC humidity reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_humidity" + return self.entity_description.value_fn(self._device) @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.humidity - - -class PuritySensor(SHCEntity, SensorEntity): - """Representation of an SHC purity reporting sensor.""" - - _attr_translation_key = "purity" - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC purity reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_purity" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.purity - - -class AirQualitySensor(SHCEntity, SensorEntity): - """Representation of an SHC airquality reporting sensor.""" - - _attr_translation_key = "air_quality" - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC airquality reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_airquality" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.combined_rating.name - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" - return { - "rating_description": self._device.description, - } - - -class TemperatureRatingSensor(SHCEntity, SensorEntity): - """Representation of an SHC temperature rating sensor.""" - - _attr_translation_key = "temperature_rating" - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC temperature rating sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_temperature_rating" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.temperature_rating.name - - -class CommunicationQualitySensor(SHCEntity, SensorEntity): - """Representation of an SHC communication quality reporting sensor.""" - - _attr_translation_key = "communication_quality" - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC communication quality reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_communication_quality" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.communicationquality.name - - -class HumidityRatingSensor(SHCEntity, SensorEntity): - """Representation of an SHC humidity rating sensor.""" - - _attr_translation_key = "humidity_rating" - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC humidity rating sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_humidity_rating" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.humidity_rating.name - - -class PurityRatingSensor(SHCEntity, SensorEntity): - """Representation of an SHC purity rating sensor.""" - - _attr_translation_key = "purity_rating" - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC purity rating sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_purity_rating" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.purity_rating.name - - -class PowerSensor(SHCEntity, SensorEntity): - """Representation of an SHC power reporting sensor.""" - - _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = UnitOfPower.WATT - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC power reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_power" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.powerconsumption - - -class EnergySensor(SHCEntity, SensorEntity): - """Representation of an SHC energy reporting sensor.""" - - _attr_device_class = SensorDeviceClass.ENERGY - _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC energy reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{self._device.serial}_energy" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.energyconsumption / 1000.0 - - -class ValveTappetSensor(SHCEntity, SensorEntity): - """Representation of an SHC valve tappet reporting sensor.""" - - _attr_translation_key = "valvetappet" - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: - """Initialize an SHC valve tappet reporting sensor.""" - super().__init__(device, parent_id, entry_id) - self._attr_unique_id = f"{device.serial}_valvetappet" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._device.position - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - "valve_tappet_state": self._device.valvestate.name, - } + if self.entity_description.attributes_fn is not None: + return self.entity_description.attributes_fn(self._device) + return None diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index e6ccd2aa9aa..58370a120f2 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -43,21 +43,21 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = { "smartplug": SHCSwitchEntityDescription( key="smartplug", device_class=SwitchDeviceClass.OUTLET, - on_key="state", + on_key="switchstate", on_value=SHCSmartPlug.PowerSwitchService.State.ON, should_poll=False, ), "smartplugcompact": SHCSwitchEntityDescription( key="smartplugcompact", device_class=SwitchDeviceClass.OUTLET, - on_key="state", + on_key="switchstate", on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON, should_poll=False, ), "lightswitch": SHCSwitchEntityDescription( key="lightswitch", device_class=SwitchDeviceClass.SWITCH, - on_key="state", + on_key="switchstate", on_value=SHCLightSwitch.PowerSwitchService.State.ON, should_poll=False, ), diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 9027a8372ab..6593afb75d1 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -12,9 +12,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import DOMAIN from .coordinator import BraviaTVCoordinator +BraviaTVConfigEntry = ConfigEntry[BraviaTVCoordinator] + PLATFORMS: Final[list[Platform]] = [ Platform.BUTTON, Platform.MEDIA_PLAYER, @@ -22,7 +23,9 @@ PLATFORMS: Final[list[Platform]] = [ ] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: BraviaTVConfigEntry +) -> bool: """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] @@ -40,26 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: BraviaTVConfigEntry +) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def update_listener( + hass: HomeAssistant, config_entry: BraviaTVConfigEntry +) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py index 0b502a3773b..358255bd85b 100644 --- a/homeassistant/components/braviatv/button.py +++ b/homeassistant/components/braviatv/button.py @@ -10,12 +10,11 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -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 +from . import BraviaTVConfigEntry from .coordinator import BraviaTVCoordinator from .entity import BraviaTVEntity @@ -45,12 +44,12 @@ BUTTONS: tuple[BraviaTVButtonDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BraviaTVConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Bravia TV Button entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data unique_id = config_entry.unique_id assert unique_id is not None diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py index b74a8a3ebdb..0969674d5c9 100644 --- a/homeassistant/components/braviatv/diagnostics.py +++ b/homeassistant/components/braviatv/diagnostics.py @@ -3,21 +3,19 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import BraviaTVCoordinator +from . import BraviaTVConfigEntry TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: BraviaTVConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: BraviaTVCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data device_info = await coordinator.client.get_system_info() diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index ea4f3cce4a8..8d45cf4a439 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -15,22 +15,22 @@ from homeassistant.components.media_player import ( MediaType, ) from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, SourceType +from . import BraviaTVConfigEntry +from .const import SourceType from .entity import BraviaTVEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BraviaTVConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bravia TV Media Player from a config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data unique_id = config_entry.unique_id assert unique_id is not None diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 01d1bb6378c..9344d6ec455 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -6,22 +6,21 @@ from collections.abc import Iterable from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import BraviaTVConfigEntry from .entity import BraviaTVEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BraviaTVConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bravia TV Remote from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data unique_id = config_entry.unique_id assert unique_id is not None diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index e408001e458..003daa64beb 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -24,8 +24,10 @@ PLATFORMS: list[Platform] = [Platform.TODO] _LOGGER = logging.getLogger(__name__) +BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool: """Set up Bring! from a config entry.""" email = entry.data[CONF_EMAIL] @@ -57,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = BringDataUpdateCoordinator(hass, bring) 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) @@ -66,7 +68,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if 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/bring/todo.py b/homeassistant/components/bring/todo.py index 5eabcc01553..56527389dd5 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -15,7 +15,6 @@ from homeassistant.components.todo import ( TodoListEntity, TodoListEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform @@ -23,6 +22,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import BringConfigEntry from .const import ( ATTR_ITEM_NAME, ATTR_NOTIFICATION_TYPE, @@ -34,11 +34,11 @@ from .coordinator import BringData, BringDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BringConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor from a config entry created in the integrations UI.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data unique_id = config_entry.unique_id diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 55368e5ff59..77c9ea0ff98 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -373,8 +373,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): start_time = dt_util.utcnow() while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) - found = await device.async_request(device.api.check_frequency)[0] - if found: + is_found, frequency = await device.async_request( + device.api.check_frequency + ) + if is_found: + _LOGGER.info("Radiofrequency detected: %s MHz", frequency) break else: await device.async_request(device.api.cancel_sweep_frequency) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 0bd49ed5d7a..08376574dcf 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -2,29 +2,23 @@ from __future__ import annotations -from asyncio import timeout -from datetime import timedelta -import logging +from brother import Brother, SnmpError -from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError - -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP +from .const import DOMAIN, SNMP +from .coordinator import BrotherDataUpdateCoordinator from .utils import get_snmp_engine PLATFORMS = [Platform.SENSOR] -SCAN_INTERVAL = timedelta(seconds=30) - -_LOGGER = logging.getLogger(__name__) +BrotherConfigEntry = ConfigEntry[BrotherDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool: """Set up Brother from a config entry.""" host = entry.data[CONF_HOST] printer_type = entry.data[CONF_TYPE] @@ -40,48 +34,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = BrotherDataUpdateCoordinator(hass, brother) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator - hass.data[DOMAIN][SNMP] = snmp_engine + entry.runtime_data = coordinator + hass.data.setdefault(DOMAIN, {SNMP: snmp_engine}) 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: BrotherConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) - if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: - hass.data[DOMAIN].pop(SNMP) - hass.data[DOMAIN].pop(DATA_CONFIG_ENTRY) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + # We only want to remove the SNMP engine when unloading the last config entry + if unload_ok and len(loaded_entries) == 1: + hass.data[DOMAIN].pop(SNMP) return unload_ok - - -class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching Brother data from the printer.""" - - def __init__(self, hass: HomeAssistant, brother: Brother) -> None: - """Initialize.""" - self.brother = brother - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - ) - - async def _async_update_data(self) -> BrotherSensors: - """Update data via library.""" - try: - async with timeout(20): - data = await self.brother.async_update() - except (ConnectionError, SnmpError, UnsupportedModelError) as error: - raise UpdateFailed(error) from error - return data diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index fda815ceee5..f8d29363acd 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -2,12 +2,13 @@ from __future__ import annotations +from datetime import timedelta from typing import Final -DATA_CONFIG_ENTRY: Final = "config_entry" - DOMAIN: Final = "brother" PRINTER_TYPES: Final = ["laser", "ink"] SNMP: Final = "snmp" + +UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/brother/coordinator.py b/homeassistant/components/brother/coordinator.py new file mode 100644 index 00000000000..69463d107e4 --- /dev/null +++ b/homeassistant/components/brother/coordinator.py @@ -0,0 +1,37 @@ +"""Coordinator for Brother integration.""" + +from asyncio import timeout +import logging + +from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): + """Class to manage fetching Brother data from the printer.""" + + def __init__(self, hass: HomeAssistant, brother: Brother) -> None: + """Initialize.""" + self.brother = brother + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> BrotherSensors: + """Update data via library.""" + try: + async with timeout(20): + data = await self.brother.async_update() + except (ConnectionError, SnmpError, UnsupportedModelError) as error: + raise UpdateFailed(error) from error + return data diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index ee5eedd84cb..d4a6c6c5400 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -5,20 +5,16 @@ from __future__ import annotations from dataclasses import asdict from typing import Any -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import BrotherDataUpdateCoordinator -from .const import DATA_CONFIG_ENTRY, DOMAIN +from . import BrotherConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: BrotherConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: BrotherDataUpdateCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ] + coordinator = config_entry.runtime_data return { "info": dict(config_entry.data), diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 6f56eb680be..e86eb59d6bc 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -25,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 BrotherDataUpdateCoordinator -from .const import DATA_CONFIG_ENTRY, DOMAIN +from . import BrotherConfigEntry, BrotherDataUpdateCoordinator +from .const import DOMAIN ATTR_COUNTER = "counter" ATTR_REMAINING_PAGES = "remaining_pages" @@ -318,11 +317,12 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: BrotherConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Brother entities from a config_entry.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] - + coordinator = entry.runtime_data # Due to the change of the attribute name of one sensor, it is necessary to migrate # the unique_id to the new one. entity_registry = er.async_get(hass) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 717a55b2027..2387c2a73c3 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -7,21 +7,21 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started -from .const import DOMAIN from .coordinator import CertExpiryDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +CertExpiryConfigEntry = ConfigEntry[CertExpiryDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -> bool: """Load the saved entities.""" host: str = entry.data[CONF_HOST] port: int = entry.data[CONF_PORT] coordinator = CertExpiryDataUpdateCoordinator(hass, host, port) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 6a55e630a35..674f7bb6341 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CertExpiryDataUpdateCoordinator +from . import CertExpiryConfigEntry, CertExpiryDataUpdateCoordinator from .const import DEFAULT_PORT, DOMAIN SCAN_INTERVAL = timedelta(hours=12) @@ -62,15 +62,13 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: CertExpiryConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add cert-expiry entry.""" - coordinator: CertExpiryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data - sensors = [ - SSLCertificateTimestamp(coordinator), - ] + sensors = [SSLCertificateTimestamp(coordinator)] async_add_entities(sensors, True) diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index beda7ba1550..e582dacf284 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -142,6 +142,9 @@ async def websocket_list_agents( agent = manager.async_get_agent(agent_info.id) assert agent is not None + if isinstance(agent, ConversationEntity): + continue + supported_languages = agent.supported_languages if language and supported_languages != MATCH_ALL: supported_languages = language_util.matches( diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index 94999d26d10..9aab2572957 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.notify import DOMAIN, NotifyEntity +from homeassistant.components.notify import DOMAIN, NotifyEntity, NotifyEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo @@ -33,12 +33,15 @@ class DemoNotifyEntity(NotifyEntity): ) -> None: """Initialize the Demo button entity.""" self._attr_unique_id = unique_id + self._attr_supported_features = NotifyEntityFeature.TITLE self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=device_name, ) - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message to a user.""" - event_notitifcation = {"message": message} - self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation) + event_notification = {"message": message} + if title is not None: + event_notification["title"] = title + self.hass.bus.async_fire(EVENT_NOTIFY, event_notification) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 78e536209d1..cbdc02e44c8 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -18,19 +18,15 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceEntry -from .const import ( - CONF_MYDEVOLO, - DEFAULT_MYDEVOLO, - DOMAIN, - GATEWAY_SERIAL_PATTERN, - PLATFORMS, -) +from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, GATEWAY_SERIAL_PATTERN, PLATFORMS + +DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: DevoloHomeControlConfigEntry +) -> bool: """Set up the devolo account from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - mydevolo = configure_mydevolo(entry.data) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) @@ -47,11 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: uuid = await hass.async_add_executor_job(mydevolo.uuid) hass.config_entries.async_update_entry(entry, unique_id=uuid) + def shutdown(event: Event) -> None: + for gateway in entry.runtime_data: + gateway.websocket_disconnect( + f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}" + ) + + # Listen when EVENT_HOMEASSISTANT_STOP is fired + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + ) + try: zeroconf_instance = await zeroconf.async_get_instance(hass) - hass.data[DOMAIN][entry.entry_id] = {"gateways": [], "listener": None} + entry.runtime_data = [] for gateway_id in gateway_ids: - hass.data[DOMAIN][entry.entry_id]["gateways"].append( + entry.runtime_data.append( await hass.async_add_executor_job( partial( HomeControl, @@ -66,31 +73,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - def shutdown(event: Event) -> None: - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - gateway.websocket_disconnect( - f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}" - ) - - # Listen when EVENT_HOMEASSISTANT_STOP is fired - hass.data[DOMAIN][entry.entry_id]["listener"] = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, shutdown - ) - return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: DevoloHomeControlConfigEntry +) -> bool: """Unload a config entry.""" unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await asyncio.gather( *( hass.async_add_executor_job(gateway.websocket_disconnect) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data ) ) - hass.data[DOMAIN][entry.entry_id]["listener"]() - hass.data[DOMAIN].pop(entry.entry_id) return unload diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 43793a15368..349780304c6 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -9,12 +9,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -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 +from . import DevoloHomeControlConfigEntry from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { @@ -28,12 +27,14 @@ DEVICE_CLASS_MAPPING = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" entities: list[BinarySensorEntity] = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: + for gateway in entry.runtime_data: entities.extend( DevoloBinaryDeviceEntity( homecontrol=gateway, diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index f94c7dae15a..29177ae2437 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -13,17 +13,18 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all cover devices and setup them via config entry.""" @@ -33,7 +34,7 @@ async def async_setup_entry( device_instance=device, element_uid=multi_level_switch, ) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data for device in gateway.multi_level_switch_devices for multi_level_switch in device.multi_level_switch_property if device.device_model_uid diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index 03aec622645..f49a9d0f0be 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -9,16 +9,17 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all cover devices and setup them via config entry.""" @@ -28,7 +29,7 @@ async def async_setup_entry( device_instance=device, element_uid=multi_level_switch, ) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data for device in gateway.multi_level_switch_devices for multi_level_switch in device.multi_level_switch_property if multi_level_switch.startswith("devolo.Blinds") diff --git a/homeassistant/components/devolo_home_control/diagnostics.py b/homeassistant/components/devolo_home_control/diagnostics.py index 33652f8e0bc..1ce65d90fd6 100644 --- a/homeassistant/components/devolo_home_control/diagnostics.py +++ b/homeassistant/components/devolo_home_control/diagnostics.py @@ -4,24 +4,19 @@ from __future__ import annotations from typing import Any -from devolo_home_control_api.homecontrol import HomeControl - from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: DevoloHomeControlConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - gateways: list[HomeControl] = hass.data[DOMAIN][entry.entry_id]["gateways"] - device_info = [ { "gateway": { @@ -38,7 +33,7 @@ async def async_get_config_entry_diagnostics( for device_id, properties in gateway.devices.items() ], } - for gateway in gateways + for gateway in entry.runtime_data ] return { diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index 36c72ca7f57..c855574b83a 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all light devices and setup them via config entry.""" @@ -27,7 +28,7 @@ async def async_setup_entry( device_instance=device, element_uid=multi_level_switch.element_uid, ) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data for device in gateway.multi_level_switch_devices for multi_level_switch in device.multi_level_switch_property.values() if multi_level_switch.switch_type == "dimmer" diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index db630cf3532..134e45a137e 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -10,12 +10,11 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { @@ -39,12 +38,14 @@ STATE_CLASS_MAPPING = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all sensor devices and setup them via config entry.""" entities: list[SensorEntity] = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: + for gateway in entry.runtime_data: entities.extend( DevoloGenericMultiLevelDeviceEntity( homecontrol=gateway, diff --git a/homeassistant/components/devolo_home_control/siren.py b/homeassistant/components/devolo_home_control/siren.py index fd015860bbb..e896f4d3ed8 100644 --- a/homeassistant/components/devolo_home_control/siren.py +++ b/homeassistant/components/devolo_home_control/siren.py @@ -6,16 +6,17 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" @@ -25,7 +26,7 @@ async def async_setup_entry( device_instance=device, element_uid=multi_level_switch, ) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data for device in gateway.multi_level_switch_devices for multi_level_switch in device.multi_level_switch_property if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch") diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index f599d39d0b6..dd3248be315 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import DevoloHomeControlConfigEntry from .devolo_device import DevoloDeviceEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DevoloHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Get all devices and setup the switch devices via config entry.""" @@ -27,7 +28,7 @@ async def async_setup_entry( device_instance=device, element_uid=binary_switch, ) - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for gateway in entry.runtime_data for device in gateway.binary_switch_devices for binary_switch in device.binary_switch_property # Exclude the binary switch which also has multi_level_switches here, diff --git a/homeassistant/components/discovergy/__init__.py b/homeassistant/components/discovergy/__init__.py index 0d38182da5d..974441f3899 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -12,16 +12,15 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client -from .const import DOMAIN from .coordinator import DiscovergyUpdateCoordinator PLATFORMS = [Platform.SENSOR] +DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> bool: """Set up Discovergy from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - client = Discovergy( email=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], @@ -53,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() coordinators.append(coordinator) - hass.data[DOMAIN][entry.entry_id] = coordinators + entry.runtime_data = coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -63,11 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index 15676da9888..3857404db81 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -6,11 +6,9 @@ from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import DiscovergyUpdateCoordinator +from . import DiscovergyConfigEntry TO_REDACT_METER = { "serial_number", @@ -22,14 +20,13 @@ TO_REDACT_METER = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: DiscovergyConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" flattened_meter: list[dict] = [] last_readings: dict[str, dict] = {} - coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] - for coordinator in coordinators: + for coordinator in entry.runtime_data: # make a dict of meter data and redact some data flattened_meter.append( async_redact_data(asdict(coordinator.meter), TO_REDACT_METER) diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index 0a820917821..531904c8740 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EntityCategory, UnitOfElectricPotential, @@ -25,6 +24,7 @@ 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 @@ -163,13 +163,13 @@ ADDITIONAL_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DiscovergyConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Discovergy sensors.""" - coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] - entities: list[DiscovergySensor] = [] - for coordinator in coordinators: + for coordinator in entry.runtime_data: sensors: tuple[DiscovergySensorEntityDescription, ...] = () # select sensor descriptions based on meter type and combine with additional sensors diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 209c77f60b5..f71b81d862b 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -2,27 +2,27 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, PLATFORMS -from .coordinator import DwdWeatherWarningsCoordinator +from .const import PLATFORMS +from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: DwdWeatherWarningsConfigEntry +) -> bool: """Set up a config entry.""" coordinator = DwdWeatherWarningsCoordinator(hass) 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: DwdWeatherWarningsConfigEntry +) -> 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/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py index 7600a04f2bb..7f0afe352db 100644 --- a/homeassistant/components/dwd_weather_warnings/coordinator.py +++ b/homeassistant/components/dwd_weather_warnings/coordinator.py @@ -19,11 +19,13 @@ from .const import ( from .exceptions import EntityNotFoundError from .util import get_position_data +DwdWeatherWarningsConfigEntry = ConfigEntry["DwdWeatherWarningsCoordinator"] + class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): """Custom coordinator for the dwd_weather_warnings integration.""" - config_entry: ConfigEntry + config_entry: DwdWeatherWarningsConfigEntry api: DwdWeatherWarningsAPI def __init__(self, hass: HomeAssistant) -> None: diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index d62c0f4f192..cef665ffb10 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -14,7 +14,6 @@ from __future__ import annotations from typing import Any from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,7 +39,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, ) -from .coordinator import DwdWeatherWarningsCoordinator +from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -55,10 +54,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DwdWeatherWarningsConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities from config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( [ @@ -80,7 +81,7 @@ class DwdWeatherWarningsSensor( def __init__( self, coordinator: DwdWeatherWarningsCoordinator, - entry: ConfigEntry, + entry: DwdWeatherWarningsConfigEntry, description: SensorEntityDescription, ) -> None: """Initialize a DWD-Weather-Warnings sensor.""" diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index 787130c403f..f7e2f1549d1 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -85,6 +85,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity): f"{self.thermostat["identifier"]}_notify_{thermostat_index}" ) - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/govee_light_local/light.py b/homeassistant/components/govee_light_local/light.py index 836f48d2ea9..60bf07e8e19 100644 --- a/homeassistant/components/govee_light_local/light.py +++ b/homeassistant/components/govee_light_local/light.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -94,7 +94,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity): name=device.sku, manufacturer=MANUFACTURER, model=device.sku, - connections={(CONNECTION_NETWORK_MAC, device.fingerprint)}, + serial_number=device.fingerprint, ) @property diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index cb7955f5407..df72a082190 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.4.4"] + "requirements": ["govee-local-api==1.4.5"] } diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 34736116a26..f5997b4a963 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,7 +1,9 @@ """The habitica integration.""" +from http import HTTPStatus import logging +from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync import voluptuous as vol @@ -16,6 +18,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -30,9 +33,12 @@ from .const import ( EVENT_API_CALL_SUCCESS, SERVICE_API_CALL, ) +from .coordinator import HabiticaDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator] + SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"] INSTANCE_SCHEMA = vol.All( @@ -104,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> bool: """Set up habitica from a config entry.""" class HAHabitipyAsync(HabitipyAsync): @@ -120,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = None for entry in entries: if entry.data[CONF_NAME] == name: - api = hass.data[DOMAIN].get(entry.entry_id) + api = entry.runtime_data.api break if api is None: _LOGGER.error("API_CALL: User '%s' not configured", name) @@ -139,24 +145,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} ) - data = hass.data.setdefault(DOMAIN, {}) - config = entry.data websession = async_get_clientsession(hass) - url = config[CONF_URL] - username = config[CONF_API_USER] - password = config[CONF_API_KEY] - name = config.get(CONF_NAME) - config_dict = {"url": url, "login": username, "password": password} - api = HAHabitipyAsync(config_dict) - user = await api.user.get() - if name is None: + + url = entry.data[CONF_URL] + username = entry.data[CONF_API_USER] + password = entry.data[CONF_API_KEY] + + api = HAHabitipyAsync( + { + "url": url, + "login": username, + "password": password, + } + ) + try: + user = await api.user.get(userFields="profile") + except ClientResponseError as e: + if e.status == HTTPStatus.TOO_MANY_REQUESTS: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="setup_rate_limit_exception", + ) from e + raise ConfigEntryNotReady(e) from e + + if not entry.data.get(CONF_NAME): name = user["profile"]["name"] hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_NAME: name}, ) - data[entry.entry_id] = api + coordinator = HabiticaDataUpdateCoordinator(hass, api) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): @@ -169,10 +191,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.config_entries.async_entries(DOMAIN)) == 1: hass.services.async_remove(DOMAIN, SERVICE_API_CALL) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py new file mode 100644 index 00000000000..385652f710a --- /dev/null +++ b/homeassistant/components/habitica/coordinator.py @@ -0,0 +1,56 @@ +"""DataUpdateCoordinator for the Habitica integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any + +from aiohttp import ClientResponseError +from habitipy.aio import HabitipyAsync + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class HabiticaData: + """Coordinator data class.""" + + user: dict[str, Any] + tasks: list[dict] + + +class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): + """Habitica Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, habitipy: HabitipyAsync) -> None: + """Initialize the Habitica data coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api = habitipy + + async def _async_update_data(self) -> HabiticaData: + user_fields = set(self.async_contexts()) + + try: + user_response = await self.api.user.get(userFields=",".join(user_fields)) + tasks_response = [] + for task_type in ("todos", "dailys", "habits", "rewards"): + tasks_response.extend(await self.api.tasks.user.get(type=task_type)) + except ClientResponseError as error: + raise UpdateFailed(f"Error communicating with API: {error}") from error + + return HabiticaData(user=user_response, tasks=tasks_response) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 7ced7cbf192..5073c31d350 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -4,13 +4,9 @@ from __future__ import annotations from collections import namedtuple from dataclasses import dataclass -from datetime import timedelta from enum import StrEnum -from http import HTTPStatus import logging -from typing import TYPE_CHECKING, Any - -from aiohttp import ClientResponseError +from typing import TYPE_CHECKING, cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,14 +18,15 @@ from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import HabiticaConfigEntry from .const import DOMAIN, MANUFACTURER, NAME +from .coordinator import HabiticaDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) - @dataclass(kw_only=True, frozen=True) class HabitipySensorEntityDescription(SensorEntityDescription): @@ -122,14 +119,14 @@ SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = { SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) TASKS_TYPES = { "habits": SensorType( - "Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"] + "Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habit"] ), "dailys": SensorType( - "Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"] + "Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["daily"] ), - "todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]), + "todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todo"]), "rewards": SensorType( - "Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"] + "Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["reward"] ), } @@ -163,79 +160,26 @@ TASKS_MAP = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HabiticaConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the habitica sensors.""" name = config_entry.data[CONF_NAME] - sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id]) - await sensor_data.update() + coordinator = config_entry.runtime_data entities: list[SensorEntity] = [ - HabitipySensor(sensor_data, description, config_entry) + HabitipySensor(coordinator, description, config_entry) for description in SENSOR_DESCRIPTIONS.values() ] entities.extend( - HabitipyTaskSensor(name, task_type, sensor_data, config_entry) + HabitipyTaskSensor(name, task_type, coordinator, config_entry) for task_type in TASKS_TYPES ) async_add_entities(entities, True) -class HabitipyData: - """Habitica API user data cache.""" - - tasks: dict[str, Any] - - def __init__(self, api) -> None: - """Habitica API user data cache.""" - self.api = api - self.data = None - self.tasks = {} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def update(self): - """Get a new fix from Habitica servers.""" - try: - self.data = await self.api.user.get() - except ClientResponseError as error: - if error.status == HTTPStatus.TOO_MANY_REQUESTS: - _LOGGER.warning( - ( - "Sensor data update for %s has too many API requests;" - " Skipping the update" - ), - DOMAIN, - ) - else: - _LOGGER.error( - "Count not update sensor data for %s (%s)", - DOMAIN, - error, - ) - - for task_type in TASKS_TYPES: - try: - self.tasks[task_type] = await self.api.tasks.user.get(type=task_type) - except ClientResponseError as error: - if error.status == HTTPStatus.TOO_MANY_REQUESTS: - _LOGGER.warning( - ( - "Sensor data update for %s has too many API requests;" - " Skipping the update" - ), - DOMAIN, - ) - else: - _LOGGER.error( - "Count not update sensor data for %s (%s)", - DOMAIN, - error, - ) - - -class HabitipySensor(SensorEntity): +class HabitipySensor(CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity): """A generic Habitica sensor.""" _attr_has_entity_name = True @@ -243,15 +187,14 @@ class HabitipySensor(SensorEntity): def __init__( self, - coordinator, + coordinator: HabiticaDataUpdateCoordinator, entity_description: HabitipySensorEntityDescription, entry: ConfigEntry, ) -> None: """Initialize a generic Habitica sensor.""" - super().__init__() + super().__init__(coordinator, context=entity_description.value_path[0]) if TYPE_CHECKING: assert entry.unique_id - self.coordinator = coordinator self.entity_description = entity_description self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( @@ -263,25 +206,27 @@ class HabitipySensor(SensorEntity): identifiers={(DOMAIN, entry.unique_id)}, ) - async def async_update(self) -> None: - """Update Sensor state.""" - await self.coordinator.update() - data = self.coordinator.data + @property + def native_value(self) -> StateType: + """Return the state of the device.""" + data = self.coordinator.data.user for element in self.entity_description.value_path: data = data[element] - self._attr_native_value = data + return cast(StateType, data) -class HabitipyTaskSensor(SensorEntity): +class HabitipyTaskSensor( + CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity +): """A Habitica task sensor.""" - def __init__(self, name, task_name, updater, entry): + def __init__(self, name, task_name, coordinator, entry): """Initialize a generic Habitica task.""" + super().__init__(coordinator) self._name = name self._task_name = task_name self._task_type = TASKS_TYPES[task_name] self._state = None - self._updater = updater self._attr_unique_id = f"{entry.unique_id}_{task_name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -292,14 +237,6 @@ class HabitipyTaskSensor(SensorEntity): identifiers={(DOMAIN, entry.unique_id)}, ) - async def async_update(self) -> None: - """Update Condition and Forecast.""" - await self._updater.update() - all_tasks = self._updater.tasks - for element in self._task_type.path: - tasks_length = len(all_tasks[element]) - self._state = tasks_length - @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -313,26 +250,29 @@ class HabitipyTaskSensor(SensorEntity): @property def native_value(self): """Return the state of the device.""" - return self._state + return len( + [ + task + for task in self.coordinator.data.tasks + if task.get("type") in self._task_type.path + ] + ) @property def extra_state_attributes(self): """Return the state attributes of all user tasks.""" - if self._updater.tasks is not None: - all_received_tasks = self._updater.tasks - for element in self._task_type.path: - received_tasks = all_received_tasks[element] - attrs = {} + attrs = {} - # Map tasks to TASKS_MAP - for received_task in received_tasks: + # Map tasks to TASKS_MAP + for received_task in self.coordinator.data.tasks: + if received_task.get("type") in self._task_type.path: task_id = received_task[TASKS_MAP_ID] task = {} for map_key, map_value in TASKS_MAP.items(): if value := received_task.get(map_value): task[map_key] = value attrs[task_id] = task - return attrs + return attrs @property def native_unit_of_measurement(self): diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 6be2bd7ed09..6023aa2d228 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -59,6 +59,11 @@ } } }, + "exceptions": { + "setup_rate_limit_exception": { + "message": "Currently rate limited, try again later" + } + }, "services": { "api_call": { "name": "API name", diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index ff63d66230d..f9a1cc54c7a 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -35,7 +35,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -99,7 +103,7 @@ async def async_setup_entry( heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE) data: HoneywellData = hass.data[DOMAIN][entry.entry_id] - + _async_migrate_unique_id(hass, data.devices) async_add_entities( [ HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) @@ -109,6 +113,21 @@ async def async_setup_entry( remove_stale_devices(hass, entry, data.devices) +def _async_migrate_unique_id( + hass: HomeAssistant, devices: dict[str, SomeComfortDevice] +) -> None: + """Migrate entities to string.""" + entity_registry = er.async_get(hass) + for device in devices.values(): + entity_id = entity_registry.async_get_entity_id( + "climate", DOMAIN, device.deviceid + ) + if entity_id is not None: + entity_registry.async_update_entity( + entity_id, new_unique_id=str(device.deviceid) + ) + + def remove_stale_devices( hass: HomeAssistant, config_entry: ConfigEntry, @@ -161,7 +180,7 @@ class HoneywellUSThermostat(ClimateEntity): self._away = False self._retry = 0 - self._attr_unique_id = device.deviceid + self._attr_unique_id = str(device.deviceid) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.deviceid)}, diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 62ed4d42a07..6f93ce71d84 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -75,7 +75,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, # The default for new entries is to not include text and headers - vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, + vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR, } ) CONFIG_SCHEMA_ADVANCED = { diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 63f6146be84..2b04482e2fb 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", - "requirements": ["imgw_pib==1.0.0"] + "requirements": ["imgw_pib==1.0.1"] } diff --git a/homeassistant/components/kitchen_sink/notify.py b/homeassistant/components/kitchen_sink/notify.py index b0418411145..fb34a36f0b7 100644 --- a/homeassistant/components/kitchen_sink/notify.py +++ b/homeassistant/components/kitchen_sink/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.components import persistent_notification -from homeassistant.components.notify import NotifyEntity +from homeassistant.components.notify import NotifyEntity, NotifyEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo @@ -25,6 +25,12 @@ async def async_setup_entry( device_name="MyBox", entity_name="Personal notifier", ), + DemoNotify( + unique_id="just_notify_me_title", + device_name="MyBox", + entity_name="Personal notifier with title", + supported_features=NotifyEntityFeature.TITLE, + ), ] ) @@ -40,15 +46,19 @@ class DemoNotify(NotifyEntity): unique_id: str, device_name: str, entity_name: str | None, + supported_features: NotifyEntityFeature = NotifyEntityFeature(0), ) -> None: """Initialize the Demo button entity.""" self._attr_unique_id = unique_id + self._attr_supported_features = supported_features self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=device_name, ) self._attr_name = entity_name - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send out a persistent notification.""" - persistent_notification.async_create(self.hass, message, "Demo notification") + persistent_notification.async_create( + self.hass, message, title or "Demo notification" + ) diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index f206ee62ece..9390acb2c85 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -108,6 +108,6 @@ class KNXNotify(KnxEntity, NotifyEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a notification to knx bus.""" await self._device.set(message) diff --git a/homeassistant/components/local_todo/__init__.py b/homeassistant/components/local_todo/__init__.py index 8245822bd9f..c01f5a748ec 100644 --- a/homeassistant/components/local_todo/__init__.py +++ b/homeassistant/components/local_todo/__init__.py @@ -10,19 +10,18 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util import slugify -from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME, DOMAIN +from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME from .store import LocalTodoListStore PLATFORMS: list[Platform] = [Platform.TODO] STORAGE_PATH = ".storage/local_todo.{key}.ics" +LocalTodoConfigEntry = ConfigEntry[LocalTodoListStore] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: LocalTodoConfigEntry) -> bool: """Set up Local To-do from a config entry.""" - - hass.data.setdefault(DOMAIN, {}) - path = Path(hass.config.path(STORAGE_PATH.format(key=entry.data[CONF_STORAGE_KEY]))) store = LocalTodoListStore(hass, path) try: @@ -30,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except OSError as err: raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err - hass.data[DOMAIN][entry.entry_id] = store + entry.runtime_data = store await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -39,10 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if 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) async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index ccd3d8db759..548b4fa87fe 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -14,14 +14,14 @@ from homeassistant.components.todo import ( TodoListEntity, TodoListEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util import dt as dt_util -from .const import CONF_TODO_LIST_NAME, DOMAIN +from . import LocalTodoConfigEntry +from .const import CONF_TODO_LIST_NAME from .store import LocalTodoListStore _LOGGER = logging.getLogger(__name__) @@ -63,12 +63,12 @@ def _migrate_calendar(calendar: Calendar) -> bool: async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LocalTodoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the local_todo todo platform.""" - store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id] + store = config_entry.runtime_data ics = await store.async_load() with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py index 710f942a006..f231c33a296 100644 --- a/homeassistant/components/lutron/event.py +++ b/homeassistant/components/lutron/event.py @@ -106,4 +106,4 @@ class LutronEventEntity(LutronKeypad, EventEntity): } self.hass.bus.fire("lutron_event", data) self._trigger_event(action) - self.async_write_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 9d80ebc38f6..da72798dda1 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -398,6 +398,8 @@ class MatterLight(MatterEntity, LightEntity): def _check_transition_blocklist(self) -> None: """Check if this device is reported to have non working transitions.""" device_info = self._endpoint.device_info + if isinstance(device_info, clusters.BridgedDeviceBasicInformation): + return if ( device_info.vendorID, device_info.productID, diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index ec402a16489..540a7867203 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -21,8 +21,12 @@ PLATFORMS = [Platform.WEATHER] _LOGGER = logging.getLogger(__name__) +MetWeatherConfigEntry = ConfigEntry[MetDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: MetWeatherConfigEntry +) -> bool: """Set up Met as config entry.""" # Don't setup if tracking home location and latitude or longitude isn't set. # Also, filters out our onboarding default location. @@ -44,10 +48,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config_entry.data.get(CONF_TRACK_HOME, False): coordinator.track_home() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry)) + config_entry.async_on_unload(coordinator.untrack_home) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -56,19 +60,14 @@ 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: MetWeatherConfigEntry +) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - - hass.data[DOMAIN][config_entry.entry_id].untrack_home() - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) -async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_update_entry(hass: HomeAssistant, config_entry: MetWeatherConfigEntry): """Reload Met component when options changed.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index d0ee4f275ea..809bb792b2c 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -21,7 +21,6 @@ from homeassistant.components.weather import ( SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -37,6 +36,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import METRIC_SYSTEM +from . import MetWeatherConfigEntry from .const import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -53,11 +53,11 @@ DEFAULT_NAME = "Met.no" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MetWeatherConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data entity_registry = er.async_get(hass) name: str | None @@ -120,7 +120,7 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]): def __init__( self, coordinator: MetDataUpdateCoordinator, - config_entry: ConfigEntry, + config_entry: MetWeatherConfigEntry, name: str, is_metric: bool, ) -> None: diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 33fec874611..5eeddee8dd4 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -91,8 +91,6 @@ class MetOfficeWeather( CoordinatorWeatherEntity[ TimestampDataUpdateCoordinator[MetOfficeData], TimestampDataUpdateCoordinator[MetOfficeData], - TimestampDataUpdateCoordinator[MetOfficeData], - TimestampDataUpdateCoordinator[MetOfficeData], # Can be removed in Python 3.12 ] ): """Implementation of a Met Office weather condition.""" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index c848c2955fb..2c5d921e1db 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -218,8 +218,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: - """Handle re-authentication with Aladdin Connect.""" - + """Handle re-authentication with MQTT broker.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py index b7a17f07f7f..07ab0050b45 100644 --- a/homeassistant/components/mqtt/notify.py +++ b/homeassistant/components/mqtt/notify.py @@ -83,7 +83,7 @@ class MqttNotify(MqttEntity, NotifyEntity): async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" payload = self._command_template(message) await self.async_publish( diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 63fd6af9295..436838d27a0 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -2,17 +2,14 @@ from __future__ import annotations -import asyncio import logging -from typing import cast +from typing import TYPE_CHECKING from aiohttp.client_exceptions import ClientConnectorError, ClientError from nettigo_air_monitor import ( ApiError, AuthFailedError, ConnectionOptions, - InvalidSensorDataError, - NAMSensors, NettigoAirMonitor, ) @@ -21,25 +18,20 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - ATTR_SDS011, - ATTR_SPS30, - DEFAULT_UPDATE_INTERVAL, - DOMAIN, - MANUFACTURER, -) +from .const import ATTR_SDS011, ATTR_SPS30, DOMAIN +from .coordinator import NAMDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BUTTON, Platform.SENSOR] +NAMConfigEntry = ConfigEntry[NAMDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool: """Set up Nettigo as config entry.""" host: str = entry.data[CONF_HOST] username: str | None = entry.data.get(CONF_USERNAME) @@ -60,11 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AuthFailedError as err: raise ConfigEntryAuthFailed from err + if TYPE_CHECKING: + assert entry.unique_id + coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -81,57 +75,6 @@ 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: NAMConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - -class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching Nettigo Air Monitor data.""" - - def __init__( - self, - hass: HomeAssistant, - nam: NettigoAirMonitor, - unique_id: str | None, - ) -> None: - """Initialize.""" - self._unique_id = unique_id - self.nam = nam - - super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL - ) - - async def _async_update_data(self) -> NAMSensors: - """Update data via library.""" - try: - async with asyncio.timeout(10): - data = await self.nam.async_update() - # We do not need to catch AuthFailed exception here because sensor data is - # always available without authorization. - except (ApiError, ClientConnectorError, InvalidSensorDataError) as error: - raise UpdateFailed(error) from error - - return data - - @property - def unique_id(self) -> str | None: - """Return a unique_id.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, - name="Nettigo Air Monitor", - sw_version=self.nam.software_version, - manufacturer=MANUFACTURER, - configuration_url=f"http://{self.nam.host}/", - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index b414e5c5525..8ac56f3d70e 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -9,14 +9,12 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NAMDataUpdateCoordinator -from .const import DOMAIN +from . import NAMConfigEntry, NAMDataUpdateCoordinator PARALLEL_UPDATES = 1 @@ -30,10 +28,10 @@ RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data buttons: list[NAMButton] = [] buttons.append(NAMButton(coordinator, RESTART_BUTTON)) diff --git a/homeassistant/components/nam/coordinator.py b/homeassistant/components/nam/coordinator.py new file mode 100644 index 00000000000..ec99b3dfb17 --- /dev/null +++ b/homeassistant/components/nam/coordinator.py @@ -0,0 +1,57 @@ +"""The Nettigo Air Monitor coordinator.""" + +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from nettigo_air_monitor import ( + ApiError, + InvalidSensorDataError, + NAMSensors, + NettigoAirMonitor, +) + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]): + """Class to manage fetching Nettigo Air Monitor data.""" + + def __init__( + self, + hass: HomeAssistant, + nam: NettigoAirMonitor, + unique_id: str, + ) -> None: + """Initialize.""" + self.unique_id = unique_id + self.device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, unique_id)}, + name="Nettigo Air Monitor", + sw_version=nam.software_version, + manufacturer=MANUFACTURER, + configuration_url=f"http://{nam.host}/", + ) + self.nam = nam + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL + ) + + async def _async_update_data(self) -> NAMSensors: + """Update data via library.""" + try: + async with asyncio.timeout(10): + data = await self.nam.async_update() + # We do not need to catch AuthFailed exception here because sensor data is + # always available without authorization. + except (ApiError, ClientConnectorError, InvalidSensorDataError) as error: + raise UpdateFailed(error) from error + + return data diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index db1a97d8fb1..d29eb40ced7 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -6,21 +6,19 @@ from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import NAMDataUpdateCoordinator -from .const import DOMAIN +from . import NAMConfigEntry TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: NAMConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data return { "info": async_redact_data(config_entry.data, TO_REDACT), diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 7b1c584c293..d4638cbdbbe 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["nettigo_air_monitor"], "quality_scale": "platinum", - "requirements": ["nettigo-air-monitor==3.0.0"], + "requirements": ["nettigo-air-monitor==3.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index a098f48e434..0f4647d071f 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -33,7 +32,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow -from . import NAMDataUpdateCoordinator +from . import NAMConfigEntry, NAMDataUpdateCoordinator from .const import ( ATTR_BME280_HUMIDITY, ATTR_BME280_PRESSURE, @@ -347,10 +346,10 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data # Due to the change of the attribute name of two sensors, it is necessary to migrate # the unique_ids to the new names. diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 11d2a85d851..209a618ec3d 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -30,8 +30,10 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) +NextcloudConfigEntry = ConfigEntry[NextcloudDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: NextcloudConfigEntry) -> bool: """Set up the Nextcloud integration.""" # migrate old entity unique ids @@ -71,17 +73,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: NextcloudConfigEntry) -> bool: """Unload Nextcloud integration.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 6c6f6141975..c9d19efbd45 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -8,13 +8,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 -from .coordinator import NextcloudDataUpdateCoordinator +from . import NextcloudConfigEntry from .entity import NextcloudEntity BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [ @@ -54,10 +52,12 @@ BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NextcloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Nextcloud binary sensors.""" - coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( NextcloudBinarySensor(coordinator, entry, sensor) for sensor in BINARY_SENSORS diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 19431756e43..6632b2674eb 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -2,11 +2,11 @@ from urllib.parse import urlparse -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import NextcloudConfigEntry from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -19,7 +19,7 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): def __init__( self, coordinator: NextcloudDataUpdateCoordinator, - entry: ConfigEntry, + entry: NextcloudConfigEntry, description: EntityDescription, ) -> None: """Initialize the Nextcloud sensor.""" diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index d8a2a362ce0..19ac7bb0df7 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp -from .const import DOMAIN -from .coordinator import NextcloudDataUpdateCoordinator +from . import NextcloudConfigEntry from .entity import NextcloudEntity UNIT_OF_LOAD: Final[str] = "load" @@ -602,10 +600,12 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NextcloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Nextcloud sensors.""" - coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( NextcloudSensor(coordinator, entry, sensor) for sensor in SENSORS diff --git a/homeassistant/components/nextcloud/update.py b/homeassistant/components/nextcloud/update.py index 52583d690bf..8c292e1bba2 100644 --- a/homeassistant/components/nextcloud/update.py +++ b/homeassistant/components/nextcloud/update.py @@ -3,20 +3,20 @@ from __future__ import annotations from homeassistant.components.update import UpdateEntity, UpdateEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .coordinator import NextcloudDataUpdateCoordinator +from . import NextcloudConfigEntry from .entity import NextcloudEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NextcloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Nextcloud update entity.""" - coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if coordinator.data.get("update_available") is None: return async_add_entities( diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index c7e4a0842fb..f76e8755734 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -3,10 +3,21 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from aiohttp.client_exceptions import ClientConnectorError -from nextdns import ApiError, NextDns +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + ConnectionStatus, + NextDns, + Settings, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform @@ -23,7 +34,6 @@ from .const import ( ATTR_SETTINGS, ATTR_STATUS, CONF_PROFILE_ID, - DOMAIN, UPDATE_INTERVAL_ANALYTICS, UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_SETTINGS, @@ -39,6 +49,22 @@ from .coordinator import ( NextDnsUpdateCoordinator, ) +NextDnsConfigEntry = ConfigEntry["NextDnsData"] + + +@dataclass +class NextDnsData: + """Data for the NextDNS integration.""" + + connection: NextDnsUpdateCoordinator[ConnectionStatus] + dnssec: NextDnsUpdateCoordinator[AnalyticsDnssec] + encryption: NextDnsUpdateCoordinator[AnalyticsEncryption] + ip_versions: NextDnsUpdateCoordinator[AnalyticsIpVersions] + protocols: NextDnsUpdateCoordinator[AnalyticsProtocols] + settings: NextDnsUpdateCoordinator[Settings] + status: NextDnsUpdateCoordinator[AnalyticsStatus] + + PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ (ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION), @@ -51,7 +77,7 @@ COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> bool: """Set up NextDNS as config entry.""" api_key = entry.data[CONF_API_KEY] profile_id = entry.data[CONF_PROFILE_ID] @@ -75,18 +101,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*tasks) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + entry.runtime_data = NextDnsData(**coordinators) 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: NextDnsConfigEntry) -> bool: """Unload a config entry.""" - unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py index c4ab58537cd..08a1f89418f 100644 --- a/homeassistant/components/nextdns/binary_sensor.py +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from nextdns import ConnectionStatus @@ -13,36 +12,33 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_CONNECTION, DOMAIN -from .coordinator import CoordinatorDataT, NextDnsConnectionUpdateCoordinator +from . import NextDnsConfigEntry +from .coordinator import NextDnsUpdateCoordinator PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) -class NextDnsBinarySensorEntityDescription( - BinarySensorEntityDescription, Generic[CoordinatorDataT] -): +class NextDnsBinarySensorEntityDescription(BinarySensorEntityDescription): """NextDNS binary sensor entity description.""" - state: Callable[[CoordinatorDataT, str], bool] + state: Callable[[ConnectionStatus, str], bool] SENSORS = ( - NextDnsBinarySensorEntityDescription[ConnectionStatus]( + NextDnsBinarySensorEntityDescription( key="this_device_nextdns_connection_status", entity_category=EntityCategory.DIAGNOSTIC, translation_key="device_connection_status", device_class=BinarySensorDeviceClass.CONNECTIVITY, state=lambda data, _: data.connected, ), - NextDnsBinarySensorEntityDescription[ConnectionStatus]( + NextDnsBinarySensorEntityDescription( key="this_device_profile_connection_status", entity_category=EntityCategory.DIAGNOSTIC, translation_key="device_profile_connection_status", @@ -54,13 +50,11 @@ SENSORS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: NextDnsConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add NextDNS entities from a config_entry.""" - coordinator: NextDnsConnectionUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - ATTR_CONNECTION - ] + coordinator = entry.runtime_data.connection async_add_entities( NextDnsBinarySensor(coordinator, description) for description in SENSORS @@ -68,7 +62,7 @@ async def async_setup_entry( class NextDnsBinarySensor( - CoordinatorEntity[NextDnsConnectionUpdateCoordinator], BinarySensorEntity + CoordinatorEntity[NextDnsUpdateCoordinator[ConnectionStatus]], BinarySensorEntity ): """Define an NextDNS binary sensor.""" @@ -77,7 +71,7 @@ class NextDnsBinarySensor( def __init__( self, - coordinator: NextDnsConnectionUpdateCoordinator, + coordinator: NextDnsUpdateCoordinator[ConnectionStatus], description: NextDnsBinarySensorEntityDescription, ) -> None: """Initialize.""" diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index d61c953f260..164d725b393 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -2,15 +2,16 @@ from __future__ import annotations +from nextdns import AnalyticsStatus + from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_STATUS, DOMAIN -from .coordinator import NextDnsStatusUpdateCoordinator +from . import NextDnsConfigEntry +from .coordinator import NextDnsUpdateCoordinator PARALLEL_UPDATES = 1 @@ -22,27 +23,26 @@ CLEAR_LOGS_BUTTON = ButtonEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NextDnsConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add aNextDNS entities from a config_entry.""" - coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - ATTR_STATUS - ] + coordinator = entry.runtime_data.status - buttons: list[NextDnsButton] = [] - buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)) - - async_add_entities(buttons) + async_add_entities([NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)]) -class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): +class NextDnsButton( + CoordinatorEntity[NextDnsUpdateCoordinator[AnalyticsStatus]], ButtonEntity +): """Define an NextDNS button.""" _attr_has_entity_name = True def __init__( self, - coordinator: NextDnsStatusUpdateCoordinator, + coordinator: NextDnsUpdateCoordinator[AnalyticsStatus], description: ButtonEntityDescription, ) -> None: """Initialize.""" diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index cade6476d82..31c0b7f0ca8 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -6,36 +6,25 @@ from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from .const import ( - ATTR_DNSSEC, - ATTR_ENCRYPTION, - ATTR_IP_VERSIONS, - ATTR_PROTOCOLS, - ATTR_SETTINGS, - ATTR_STATUS, - CONF_PROFILE_ID, - DOMAIN, -) +from . import NextDnsConfigEntry +from .const import CONF_PROFILE_ID TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: NextDnsConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators = hass.data[DOMAIN][config_entry.entry_id] - - dnssec_coordinator = coordinators[ATTR_DNSSEC] - encryption_coordinator = coordinators[ATTR_ENCRYPTION] - ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] - protocols_coordinator = coordinators[ATTR_PROTOCOLS] - settings_coordinator = coordinators[ATTR_SETTINGS] - status_coordinator = coordinators[ATTR_STATUS] + dnssec_coordinator = config_entry.runtime_data.dnssec + encryption_coordinator = config_entry.runtime_data.encryption + ip_versions_coordinator = config_entry.runtime_data.ip_versions + protocols_coordinator = config_entry.runtime_data.protocols + settings_coordinator = config_entry.runtime_data.settings + status_coordinator = config_entry.runtime_data.status return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index a034901aa41..b390ac93e06 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -19,20 +19,19 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import NextDnsConfigEntry from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, ATTR_IP_VERSIONS, ATTR_PROTOCOLS, ATTR_STATUS, - DOMAIN, ) from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator @@ -301,14 +300,14 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: NextDnsConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add a NextDNS entities from a config_entry.""" - coordinators = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - NextDnsSensor(coordinators[description.coordinator_type], description) + NextDnsSensor( + getattr(entry.runtime_data, description.coordinator_type), description + ) for description in SENSORS ) diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index a6bbead131e..37ff22c7521 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -4,518 +4,515 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic +from typing import Any from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, Settings from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_SETTINGS, DOMAIN -from .coordinator import CoordinatorDataT, NextDnsSettingsUpdateCoordinator +from . import NextDnsConfigEntry +from .coordinator import NextDnsUpdateCoordinator PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) -class NextDnsSwitchEntityDescription( - SwitchEntityDescription, Generic[CoordinatorDataT] -): +class NextDnsSwitchEntityDescription(SwitchEntityDescription): """NextDNS switch entity description.""" - state: Callable[[CoordinatorDataT], bool] + state: Callable[[Settings], bool] SWITCHES = ( - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_page", translation_key="block_page", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_page, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="cache_boost", translation_key="cache_boost", entity_category=EntityCategory.CONFIG, state=lambda data: data.cache_boost, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="cname_flattening", translation_key="cname_flattening", entity_category=EntityCategory.CONFIG, state=lambda data: data.cname_flattening, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="anonymized_ecs", translation_key="anonymized_ecs", entity_category=EntityCategory.CONFIG, state=lambda data: data.anonymized_ecs, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="logs", translation_key="logs", entity_category=EntityCategory.CONFIG, state=lambda data: data.logs, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="web3", translation_key="web3", entity_category=EntityCategory.CONFIG, state=lambda data: data.web3, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="allow_affiliate", translation_key="allow_affiliate", entity_category=EntityCategory.CONFIG, state=lambda data: data.allow_affiliate, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_disguised_trackers", translation_key="block_disguised_trackers", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_disguised_trackers, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="ai_threat_detection", translation_key="ai_threat_detection", entity_category=EntityCategory.CONFIG, state=lambda data: data.ai_threat_detection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_csam", translation_key="block_csam", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_csam, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_ddns", translation_key="block_ddns", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_ddns, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_nrd", translation_key="block_nrd", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_nrd, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_parked_domains", translation_key="block_parked_domains", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_parked_domains, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="cryptojacking_protection", translation_key="cryptojacking_protection", entity_category=EntityCategory.CONFIG, state=lambda data: data.cryptojacking_protection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="dga_protection", translation_key="dga_protection", entity_category=EntityCategory.CONFIG, state=lambda data: data.dga_protection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="dns_rebinding_protection", translation_key="dns_rebinding_protection", entity_category=EntityCategory.CONFIG, state=lambda data: data.dns_rebinding_protection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="google_safe_browsing", translation_key="google_safe_browsing", entity_category=EntityCategory.CONFIG, state=lambda data: data.google_safe_browsing, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="idn_homograph_attacks_protection", translation_key="idn_homograph_attacks_protection", entity_category=EntityCategory.CONFIG, state=lambda data: data.idn_homograph_attacks_protection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="threat_intelligence_feeds", translation_key="threat_intelligence_feeds", entity_category=EntityCategory.CONFIG, state=lambda data: data.threat_intelligence_feeds, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="typosquatting_protection", translation_key="typosquatting_protection", entity_category=EntityCategory.CONFIG, state=lambda data: data.typosquatting_protection, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_bypass_methods", translation_key="block_bypass_methods", entity_category=EntityCategory.CONFIG, state=lambda data: data.block_bypass_methods, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="safesearch", translation_key="safesearch", entity_category=EntityCategory.CONFIG, state=lambda data: data.safesearch, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="youtube_restricted_mode", translation_key="youtube_restricted_mode", entity_category=EntityCategory.CONFIG, state=lambda data: data.youtube_restricted_mode, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_9gag", translation_key="block_9gag", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_9gag, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_amazon", translation_key="block_amazon", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_amazon, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_bereal", translation_key="block_bereal", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_bereal, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_blizzard", translation_key="block_blizzard", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_blizzard, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_chatgpt", translation_key="block_chatgpt", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_chatgpt, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_dailymotion", translation_key="block_dailymotion", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_dailymotion, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_discord", translation_key="block_discord", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_discord, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_disneyplus", translation_key="block_disneyplus", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_disneyplus, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_ebay", translation_key="block_ebay", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_ebay, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_facebook", translation_key="block_facebook", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_facebook, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_fortnite", translation_key="block_fortnite", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_fortnite, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_google_chat", translation_key="block_google_chat", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_google_chat, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_hbomax", translation_key="block_hbomax", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_hbomax, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_hulu", name="Block Hulu", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_hulu, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_imgur", translation_key="block_imgur", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_imgur, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_instagram", translation_key="block_instagram", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_instagram, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_leagueoflegends", translation_key="block_leagueoflegends", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_leagueoflegends, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_mastodon", translation_key="block_mastodon", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_mastodon, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_messenger", translation_key="block_messenger", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_messenger, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_minecraft", translation_key="block_minecraft", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_minecraft, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_netflix", translation_key="block_netflix", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_netflix, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_pinterest", translation_key="block_pinterest", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_pinterest, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_playstation_network", translation_key="block_playstation_network", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_playstation_network, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_primevideo", translation_key="block_primevideo", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_primevideo, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_reddit", translation_key="block_reddit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_reddit, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_roblox", translation_key="block_roblox", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_roblox, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_signal", translation_key="block_signal", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_signal, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_skype", translation_key="block_skype", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_skype, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_snapchat", translation_key="block_snapchat", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_snapchat, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_spotify", translation_key="block_spotify", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_spotify, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_steam", translation_key="block_steam", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_steam, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_telegram", translation_key="block_telegram", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_telegram, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_tiktok", translation_key="block_tiktok", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_tiktok, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_tinder", translation_key="block_tinder", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_tinder, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_tumblr", translation_key="block_tumblr", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_tumblr, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_twitch", translation_key="block_twitch", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_twitch, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_twitter", translation_key="block_twitter", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_twitter, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_vimeo", translation_key="block_vimeo", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_vimeo, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_vk", translation_key="block_vk", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_vk, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_whatsapp", translation_key="block_whatsapp", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_whatsapp, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_xboxlive", translation_key="block_xboxlive", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_xboxlive, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_youtube", translation_key="block_youtube", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_youtube, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_zoom", translation_key="block_zoom", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_zoom, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_dating", translation_key="block_dating", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_dating, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_gambling", translation_key="block_gambling", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_gambling, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_online_gaming", translation_key="block_online_gaming", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_online_gaming, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_piracy", translation_key="block_piracy", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_piracy, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_porn", translation_key="block_porn", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_porn, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_social_networks", translation_key="block_social_networks", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, state=lambda data: data.block_social_networks, ), - NextDnsSwitchEntityDescription[Settings]( + NextDnsSwitchEntityDescription( key="block_video_streaming", translation_key="block_video_streaming", entity_category=EntityCategory.CONFIG, @@ -526,19 +523,21 @@ SWITCHES = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NextDnsConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add NextDNS entities from a config_entry.""" - coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - ATTR_SETTINGS - ] + coordinator = entry.runtime_data.settings async_add_entities( NextDnsSwitch(coordinator, description) for description in SWITCHES ) -class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): +class NextDnsSwitch( + CoordinatorEntity[NextDnsUpdateCoordinator[Settings]], SwitchEntity +): """Define an NextDNS switch.""" _attr_has_entity_name = True @@ -546,7 +545,7 @@ class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchE def __init__( self, - coordinator: NextDnsSettingsUpdateCoordinator, + coordinator: NextDnsUpdateCoordinator[Settings], description: NextDnsSwitchEntityDescription, ) -> None: """Initialize.""" diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 81b7d300acc..ce4f778993c 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from enum import IntFlag from functools import cached_property, partial import logging from typing import Any, final, override @@ -58,6 +59,12 @@ PLATFORM_SCHEMA = vol.Schema( ) +class NotifyEntityFeature(IntFlag): + """Supported features of a notify entity.""" + + TITLE = 1 + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" @@ -73,7 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass) component.async_register_entity_service( SERVICE_SEND_MESSAGE, - {vol.Required(ATTR_MESSAGE): cv.string}, + { + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_TITLE): cv.string, + }, "_async_send_message", ) @@ -128,6 +138,7 @@ class NotifyEntity(RestoreEntity): """Representation of a notify entity.""" entity_description: NotifyEntityDescription + _attr_supported_features: NotifyEntityFeature = NotifyEntityFeature(0) _attr_should_poll = False _attr_device_class: None _attr_state: None = None @@ -162,10 +173,19 @@ class NotifyEntity(RestoreEntity): self.async_write_ha_state() await self.async_send_message(**kwargs) - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" raise NotImplementedError - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" - await self.hass.async_add_executor_job(partial(self.send_message, message)) + kwargs: dict[str, Any] = {} + if ( + title is not None + and self.supported_features + and self.supported_features & NotifyEntityFeature.TITLE + ): + kwargs[ATTR_TITLE] = title + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index ae2a0254761..c4778b10618 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -29,6 +29,13 @@ send_message: required: true selector: text: + title: + required: false + selector: + text: + filter: + supported_features: + - notify.NotifyEntityFeature.TITLE persistent_notification: fields: diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index b0dca501509..f6ac8c848f1 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -35,6 +35,10 @@ "message": { "name": "Message", "description": "Your notification message." + }, + "title": { + "name": "Title", + "description": "Title for your notification message." } } }, diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 8b715237e01..640dbb1416a 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -26,22 +26,30 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, INTEGRATION_SUPPORTED_COMMANDS, PLATFORMS, - PYNUT_DATA, - PYNUT_UNIQUE_ID, - USER_AVAILABLE_COMMANDS, ) NUT_FAKE_SERIAL = ["unknown", "blank"] _LOGGER = logging.getLogger(__name__) +NutConfigEntry = ConfigEntry["NutRuntimeData"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class NutRuntimeData: + """Runtime data definition.""" + + coordinator: DataUpdateCoordinator + data: PyNUTData + unique_id: str + user_available_commands: set[str] + + +async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" # strip out the stale options CONF_RESOURCES, @@ -110,13 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: user_available_commands = set() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - COORDINATOR: coordinator, - PYNUT_DATA: data, - PYNUT_UNIQUE_ID: unique_id, - USER_AVAILABLE_COMMANDS: user_available_commands, - } + entry.runtime_data = NutRuntimeData( + coordinator, data, unique_id, user_available_commands + ) device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -135,9 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if 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) async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 9be06de1f73..6db40a910a0 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -15,15 +15,8 @@ DEFAULT_PORT = 3493 KEY_STATUS = "ups.status" KEY_STATUS_DISPLAY = "ups.status.display" -COORDINATOR = "coordinator" DEFAULT_SCAN_INTERVAL = 60 -PYNUT_DATA = "data" -PYNUT_UNIQUE_ID = "unique_id" - - -USER_AVAILABLE_COMMANDS = "user_available_commands" - STATE_TYPES = { "OL": "Online", "OB": "On Battery", diff --git a/homeassistant/components/nut/device_action.py b/homeassistant/components/nut/device_action.py index 0ec58e651b2..a051f843226 100644 --- a/homeassistant/components/nut/device_action.py +++ b/homeassistant/components/nut/device_action.py @@ -4,19 +4,15 @@ from __future__ import annotations import voluptuous as vol +from homeassistant.components.device_automation import InvalidDeviceAutomationConfig from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import PyNUTData -from .const import ( - DOMAIN, - INTEGRATION_SUPPORTED_COMMANDS, - PYNUT_DATA, - USER_AVAILABLE_COMMANDS, -) +from . import NutRuntimeData +from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS} @@ -31,18 +27,15 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Network UPS Tools (NUT) devices.""" - if (entry_id := _get_entry_id_from_device_id(hass, device_id)) is None: + if (runtime_data := _get_runtime_data_from_device_id(hass, device_id)) is None: return [] base_action = { CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, } - user_available_commands: set[str] = hass.data[DOMAIN][entry_id][ - USER_AVAILABLE_COMMANDS - ] return [ {CONF_TYPE: _get_device_action_name(command_name)} | base_action - for command_name in user_available_commands + for command_name in runtime_data.user_available_commands ] @@ -56,9 +49,12 @@ async def async_call_action_from_config( device_action_name: str = config[CONF_TYPE] command_name = _get_command_name(device_action_name) device_id: str = config[CONF_DEVICE_ID] - entry_id = _get_entry_id_from_device_id(hass, device_id) - data: PyNUTData = hass.data[DOMAIN][entry_id][PYNUT_DATA] - await data.async_run_command(command_name) + runtime_data = _get_runtime_data_from_device_id(hass, device_id) + if not runtime_data: + raise InvalidDeviceAutomationConfig( + f"Unable to find a NUT device with id {device_id}" + ) + await runtime_data.data.async_run_command(command_name) def _get_device_action_name(command_name: str) -> str: @@ -69,8 +65,14 @@ def _get_command_name(device_action_name: str) -> str: return device_action_name.replace("_", ".") -def _get_entry_id_from_device_id(hass: HomeAssistant, device_id: str) -> str | None: +def _get_runtime_data_from_device_id( + hass: HomeAssistant, device_id: str +) -> NutRuntimeData | None: device_registry = dr.async_get(hass) if (device := device_registry.async_get(device_id)) is None: return None - return next(entry for entry in device.config_entries) + entry = hass.config_entries.async_get_entry( + next(entry_id for entry_id in device.config_entries) + ) + assert entry and isinstance(entry.runtime_data, NutRuntimeData) + return entry.runtime_data diff --git a/homeassistant/components/nut/diagnostics.py b/homeassistant/components/nut/diagnostics.py index 88a05e461c9..532e4ece76b 100644 --- a/homeassistant/components/nut/diagnostics.py +++ b/homeassistant/components/nut/diagnostics.py @@ -7,27 +7,26 @@ from typing import Any import attr from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import PyNUTData -from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID, USER_AVAILABLE_COMMANDS +from . import NutConfigEntry +from .const import DOMAIN TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: NutConfigEntry ) -> dict[str, dict[str, Any]]: """Return diagnostics for a config entry.""" data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} - hass_data = hass.data[DOMAIN][entry.entry_id] + hass_data = entry.runtime_data # Get information from Nut library - nut_data: PyNUTData = hass_data[PYNUT_DATA] - nut_cmd: set[str] = hass_data[USER_AVAILABLE_COMMANDS] + nut_data = hass_data.data + nut_cmd = hass_data.user_available_commands data["nut_data"] = { "ups_list": nut_data.ups_list, "status": nut_data.status, @@ -38,7 +37,7 @@ async def async_get_config_entry_diagnostics( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) hass_device = device_registry.async_get_device( - identifiers={(DOMAIN, hass_data[PYNUT_UNIQUE_ID])} + identifiers={(DOMAIN, hass_data.unique_id)} ) if not hass_device: return data diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index cd5ae64901d..7b61342866b 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, @@ -36,16 +35,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from . import PyNUTData -from .const import ( - COORDINATOR, - DOMAIN, - KEY_STATUS, - KEY_STATUS_DISPLAY, - PYNUT_DATA, - PYNUT_UNIQUE_ID, - STATE_TYPES, -) +from . import NutConfigEntry, PyNUTData +from .const import DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, STATE_TYPES NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = { "manufacturer": ATTR_MANUFACTURER, @@ -968,15 +959,15 @@ def _get_nut_device_info(data: PyNUTData) -> DeviceInfo: async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: NutConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the NUT sensors.""" - pynut_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = pynut_data[COORDINATOR] - data = pynut_data[PYNUT_DATA] - unique_id = pynut_data[PYNUT_UNIQUE_ID] + pynut_data = config_entry.runtime_data + coordinator = pynut_data.coordinator + data = pynut_data.data + unique_id = pynut_data.unique_id status = coordinator.data resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status] diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 34157769b97..840d4d917f7 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -2,21 +2,18 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable from dataclasses import dataclass import datetime import logging -from typing import TYPE_CHECKING -from pynws import SimpleNWS +from pynws import SimpleNWS, call_with_retry from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.dt import utcnow @@ -27,8 +24,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.WEATHER] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) -FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) -DEBOUNCE_TIME = 60 # in seconds +RETRY_INTERVAL = datetime.timedelta(minutes=1) +RETRY_STOP = datetime.timedelta(minutes=10) + +DEBOUNCE_TIME = 10 * 60 # in seconds def base_unique_id(latitude: float, longitude: float) -> str: @@ -41,62 +40,9 @@ class NWSData: """Data for the National Weather Service integration.""" api: SimpleNWS - coordinator_observation: NwsDataUpdateCoordinator - coordinator_forecast: NwsDataUpdateCoordinator - coordinator_forecast_hourly: NwsDataUpdateCoordinator - - -class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module - """NWS data update coordinator. - - Implements faster data update intervals for failed updates and exposes a last successful update time. - """ - - def __init__( - self, - hass: HomeAssistant, - logger: logging.Logger, - *, - name: str, - update_interval: datetime.timedelta, - failed_update_interval: datetime.timedelta, - update_method: Callable[[], Awaitable[None]] | None = None, - request_refresh_debouncer: debounce.Debouncer | None = None, - ) -> None: - """Initialize NWS coordinator.""" - super().__init__( - hass, - logger, - name=name, - update_interval=update_interval, - update_method=update_method, - request_refresh_debouncer=request_refresh_debouncer, - ) - self.failed_update_interval = failed_update_interval - - @callback - def _schedule_refresh(self) -> None: - """Schedule a refresh.""" - if self._unsub_refresh: - self._unsub_refresh() - self._unsub_refresh = None - - # We _floor_ utcnow to create a schedule on a rounded second, - # minimizing the time between the point and the real activation. - # That way we obtain a constant update frequency, - # as long as the update process takes less than a second - if self.last_update_success: - if TYPE_CHECKING: - # the base class allows None, but this one doesn't - assert self.update_interval is not None - update_interval = self.update_interval - else: - update_interval = self.failed_update_interval - self._unsub_refresh = async_track_point_in_utc_time( - self.hass, - self._handle_refresh_interval, - utcnow().replace(microsecond=0) + update_interval, - ) + coordinator_observation: TimestampDataUpdateCoordinator[None] + coordinator_forecast: TimestampDataUpdateCoordinator[None] + coordinator_forecast_hourly: TimestampDataUpdateCoordinator[None] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -114,39 +60,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_observation() -> None: """Retrieve recent observations.""" - await nws_data.update_observation(start_time=utcnow() - UPDATE_TIME_PERIOD) + await call_with_retry( + nws_data.update_observation, + RETRY_INTERVAL, + RETRY_STOP, + start_time=utcnow() - UPDATE_TIME_PERIOD, + ) - coordinator_observation = NwsDataUpdateCoordinator( + async def update_forecast() -> None: + """Retrieve twice-daily forecsat.""" + await call_with_retry( + nws_data.update_forecast, + RETRY_INTERVAL, + RETRY_STOP, + ) + + async def update_forecast_hourly() -> None: + """Retrieve hourly forecast.""" + await call_with_retry( + nws_data.update_forecast_hourly, + RETRY_INTERVAL, + RETRY_STOP, + ) + + coordinator_observation = TimestampDataUpdateCoordinator( hass, _LOGGER, name=f"NWS observation station {station}", update_method=update_observation, update_interval=DEFAULT_SCAN_INTERVAL, - failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), ) - coordinator_forecast = NwsDataUpdateCoordinator( + coordinator_forecast = TimestampDataUpdateCoordinator( hass, _LOGGER, name=f"NWS forecast station {station}", - update_method=nws_data.update_forecast, + update_method=update_forecast, update_interval=DEFAULT_SCAN_INTERVAL, - failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), ) - coordinator_forecast_hourly = NwsDataUpdateCoordinator( + coordinator_forecast_hourly = TimestampDataUpdateCoordinator( hass, _LOGGER, name=f"NWS forecast hourly station {station}", - update_method=nws_data.update_forecast_hourly, + update_method=update_forecast_hourly, update_interval=DEFAULT_SCAN_INTERVAL, - failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 4006a145db4..f68d76ee95b 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws==1.6.0"] + "requirements": ["pynws[retry]==1.7.0"] } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 1d8c5ab045e..447c2dc5cf8 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -25,7 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + TimestampDataUpdateCoordinator, +) from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import ( DistanceConverter, @@ -34,7 +37,7 @@ from homeassistant.util.unit_conversion import ( ) from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import NWSData, NwsDataUpdateCoordinator, base_unique_id, device_info +from . import NWSData, base_unique_id, device_info from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME PARALLEL_UPDATES = 0 @@ -158,7 +161,7 @@ async def async_setup_entry( ) -class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): +class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorEntity): """An NWS Sensor Entity.""" entity_description: NWSSensorEntityDescription diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 89414f5acf1..f25998f1504 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from types import MappingProxyType from typing import TYPE_CHECKING, Any, cast @@ -34,7 +35,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utcnow +from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter from . import NWSData, base_unique_id, device_info @@ -46,7 +47,6 @@ from .const import ( DOMAIN, FORECAST_VALID_TIME, HOURLY, - OBSERVATION_VALID_TIME, ) PARALLEL_UPDATES = 0 @@ -111,7 +111,7 @@ def _calculate_unique_id(entry_data: MappingProxyType[str, Any], mode: str) -> s return f"{base_unique_id(latitude, longitude)}_{mode}" -class NWSWeather(CoordinatorWeatherEntity): +class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]]): """Representation of a weather condition.""" _attr_attribution = ATTRIBUTION @@ -140,96 +140,69 @@ class NWSWeather(CoordinatorWeatherEntity): self.nws = nws_data.api latitude = entry_data[CONF_LATITUDE] longitude = entry_data[CONF_LONGITUDE] - self.coordinator_forecast_legacy = nws_data.coordinator_forecast - self.station = self.nws.station - self.observation: dict[str, Any] | None = None - self._forecast_hourly: list[dict[str, Any]] | None = None - self._forecast_legacy: list[dict[str, Any]] | None = None - self._forecast_twice_daily: list[dict[str, Any]] | None = None + self.station = self.nws.station self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT) self._attr_device_info = device_info(latitude, longitude) self._attr_name = self.station async def async_added_to_hass(self) -> None: - """Set up a listener and load data.""" + """When entity is added to hass.""" await super().async_added_to_hass() - self.async_on_remove( - self.coordinator_forecast_legacy.async_add_listener( - self._handle_legacy_forecast_coordinator_update + self.async_on_remove(partial(self._remove_forecast_listener, "daily")) + self.async_on_remove(partial(self._remove_forecast_listener, "hourly")) + self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily")) + + for forecast_type in ("twice_daily", "hourly"): + if (coordinator := self.forecast_coordinators[forecast_type]) is None: + continue + self.unsub_forecast[forecast_type] = coordinator.async_add_listener( + partial(self._handle_forecast_update, forecast_type) ) - ) - # Load initial data from coordinators - self._handle_coordinator_update() - self._handle_hourly_forecast_coordinator_update() - self._handle_twice_daily_forecast_coordinator_update() - self._handle_legacy_forecast_coordinator_update() - - @callback - def _handle_coordinator_update(self) -> None: - """Load data from integration.""" - self.observation = self.nws.observation - self.async_write_ha_state() - - @callback - def _handle_hourly_forecast_coordinator_update(self) -> None: - """Handle updated data from the hourly forecast coordinator.""" - self._forecast_hourly = self.nws.forecast_hourly - - @callback - def _handle_twice_daily_forecast_coordinator_update(self) -> None: - """Handle updated data from the twice daily forecast coordinator.""" - self._forecast_twice_daily = self.nws.forecast - - @callback - def _handle_legacy_forecast_coordinator_update(self) -> None: - """Handle updated data from the legacy forecast coordinator.""" - self._forecast_legacy = self.nws.forecast - self.async_write_ha_state() @property def native_temperature(self) -> float | None: """Return the current temperature.""" - if self.observation: - return self.observation.get("temperature") + if observation := self.nws.observation: + return observation.get("temperature") return None @property def native_pressure(self) -> int | None: """Return the current pressure.""" - if self.observation: - return self.observation.get("seaLevelPressure") + if observation := self.nws.observation: + return observation.get("seaLevelPressure") return None @property def humidity(self) -> float | None: """Return the name of the sensor.""" - if self.observation: - return self.observation.get("relativeHumidity") + if observation := self.nws.observation: + return observation.get("relativeHumidity") return None @property def native_wind_speed(self) -> float | None: """Return the current windspeed.""" - if self.observation: - return self.observation.get("windSpeed") + if observation := self.nws.observation: + return observation.get("windSpeed") return None @property def wind_bearing(self) -> int | None: """Return the current wind bearing (degrees).""" - if self.observation: - return self.observation.get("windDirection") + if observation := self.nws.observation: + return observation.get("windDirection") return None @property def condition(self) -> str | None: """Return current condition.""" weather = None - if self.observation: - weather = self.observation.get("iconWeather") - time = cast(str, self.observation.get("iconTime")) + if observation := self.nws.observation: + weather = observation.get("iconWeather") + time = cast(str, observation.get("iconTime")) if weather: return convert_condition(time, weather) @@ -238,8 +211,8 @@ class NWSWeather(CoordinatorWeatherEntity): @property def native_visibility(self) -> int | None: """Return visibility.""" - if self.observation: - return self.observation.get("visibility") + if observation := self.nws.observation: + return observation.get("visibility") return None def _forecast( @@ -302,33 +275,12 @@ class NWSWeather(CoordinatorWeatherEntity): @callback def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" - return self._forecast(self._forecast_hourly, HOURLY) + return self._forecast(self.nws.forecast_hourly, HOURLY) @callback def _async_forecast_twice_daily(self) -> list[Forecast] | None: """Return the twice daily forecast in native units.""" - return self._forecast(self._forecast_twice_daily, DAYNIGHT) - - @property - def available(self) -> bool: - """Return if state is available.""" - last_success = ( - self.coordinator.last_update_success - and self.coordinator_forecast_legacy.last_update_success - ) - if ( - self.coordinator.last_update_success_time - and self.coordinator_forecast_legacy.last_update_success_time - ): - last_success_time = ( - utcnow() - self.coordinator.last_update_success_time - < OBSERVATION_VALID_TIME - and utcnow() - self.coordinator_forecast_legacy.last_update_success_time - < FORECAST_VALID_TIME - ) - else: - last_success_time = False - return last_success or last_success_time + return self._forecast(self.nws.forecast, DAYNIGHT) async def async_update(self) -> None: """Update the entity. @@ -336,4 +288,7 @@ class NWSWeather(CoordinatorWeatherEntity): Only used by the generic entity update service. """ await self.coordinator.async_request_refresh() - await self.coordinator_forecast_legacy.async_request_refresh() + + for forecast_type in ("twice_daily", "hourly"): + if (coordinator := self.forecast_coordinators[forecast_type]) is not None: + await coordinator.async_request_refresh() diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index 8a5f6e7d5c5..cbec719780a 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -45,7 +45,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up conversation entities.""" - agent = OllamaConversationEntity(hass, config_entry) + agent = OllamaConversationEntity(config_entry) async_add_entities([agent]) @@ -56,9 +56,8 @@ class OllamaConversationEntity( _attr_has_entity_name = True - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the agent.""" - self.hass = hass self.entry = entry # conversation id -> message history @@ -223,21 +222,21 @@ class OllamaConversationEntity( ] for state in exposed_states: - entity = entity_registry.async_get(state.entity_id) + entity_entry = entity_registry.async_get(state.entity_id) names = [state.name] area_names = [] - if entity is not None: + if entity_entry is not None: # Add aliases - names.extend(entity.aliases) - if entity.area_id and ( - area := area_registry.async_get_area(entity.area_id) + names.extend(entity_entry.aliases) + if entity_entry.area_id and ( + area := area_registry.async_get_area(entity_entry.area_id) ): # Entity is in area area_names.append(area.name) area_names.extend(area.aliases) - elif entity.device_id and ( - device := device_registry.async_get(entity.device_id) + elif entity_entry.device_id and ( + device := device_registry.async_get(entity_entry.device_id) ): # Check device area if device.area_id and ( diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 3c94d66ee4a..39549af3b88 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -35,7 +35,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up conversation entities.""" - agent = OpenAIConversationEntity(hass, config_entry) + agent = OpenAIConversationEntity(config_entry) async_add_entities([agent]) @@ -46,9 +46,8 @@ class OpenAIConversationEntity( _attr_has_entity_name = True - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the agent.""" - self.hass = hass self.entry = entry self.history: dict[str, list[dict]] = {} self._attr_name = entry.title diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index ad99416e448..f740bf6c551 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any @@ -21,20 +22,28 @@ from homeassistant.core import HomeAssistant from .const import ( CONFIG_FLOW_VERSION, - DOMAIN, - ENTRY_NAME, - ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_FREE_DAILY, FORECAST_MODE_ONECALL_DAILY, PLATFORMS, - UPDATE_LISTENER, ) from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) +OpenweathermapConfigEntry = ConfigEntry["OpenweathermapData"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class OpenweathermapData: + """Runtime data definition.""" + + name: str + coordinator: WeatherUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, entry: OpenweathermapConfigEntry +) -> bool: """Set up OpenWeatherMap as config entry.""" name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] @@ -52,17 +61,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await weather_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - ENTRY_NAME: name, - ENTRY_WEATHER_COORDINATOR: weather_coordinator, - } + entry.async_on_unload(entry.add_update_listener(async_update_options)) + + entry.runtime_data = OpenweathermapData(name, weather_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - update_listener = entry.add_update_listener(async_update_options) - hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener - return True @@ -93,15 +97,11 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: 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: OpenweathermapConfigEntry +) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] - update_listener() - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) def _get_config_value(config_entry: ConfigEntry, key: str) -> Any: diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index dbd536a2556..cae21e8f054 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -26,8 +26,6 @@ DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" MANUFACTURER = "OpenWeather" CONFIG_FLOW_VERSION = 2 -ENTRY_NAME = "name" -ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_PRECIPITATION = "precipitation" ATTR_API_PRECIPITATION_KIND = "precipitation_kind" ATTR_API_DATETIME = "datetime" diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 16d9c3064d7..70b21324b46 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEGREE, PERCENTAGE, @@ -29,6 +28,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util +from . import OpenweathermapConfigEntry from .const import ( ATTR_API_CLOUDS, ATTR_API_CONDITION, @@ -57,8 +57,6 @@ from .const import ( ATTRIBUTION, DEFAULT_NAME, DOMAIN, - ENTRY_NAME, - ENTRY_WEATHER_COORDINATOR, MANUFACTURER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -222,13 +220,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: OpenweathermapConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up OpenWeatherMap sensor entities based on a config entry.""" - domain_data = hass.data[DOMAIN][config_entry.entry_id] - name = domain_data[ENTRY_NAME] - weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + domain_data = config_entry.runtime_data + name = domain_data.name + weather_coordinator = domain_data.coordinator entities: list[AbstractOpenWeatherMapSensor] = [ OpenWeatherMapSensor( diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 62bf18ba813..406b1c8ad4b 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -21,7 +21,6 @@ from homeassistant.components.weather import ( SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( UnitOfPrecipitationDepth, UnitOfPressure, @@ -32,6 +31,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenweathermapConfigEntry from .const import ( ATTR_API_CLOUDS, ATTR_API_CONDITION, @@ -59,8 +59,6 @@ from .const import ( ATTRIBUTION, DEFAULT_NAME, DOMAIN, - ENTRY_NAME, - ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_DAILY, FORECAST_MODE_ONECALL_DAILY, MANUFACTURER, @@ -85,13 +83,13 @@ FORECAST_MAP = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: OpenweathermapConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up OpenWeatherMap weather entity based on a config entry.""" - domain_data = hass.data[DOMAIN][config_entry.entry_id] - name = domain_data[ENTRY_NAME] - weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + domain_data = config_entry.runtime_data + name = domain_data.name + weather_coordinator = domain_data.coordinator unique_id = f"{config_entry.unique_id}" owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator) diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index 9f467dce1c6..c75ffb9614b 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -69,7 +69,6 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill electric cost to date", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -79,7 +78,6 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill electric forecasted cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -89,7 +87,6 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Typical monthly electric cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, @@ -101,7 +98,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill gas usage to date", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.usage_to_date, @@ -111,7 +107,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill gas forecasted usage", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_usage, @@ -121,7 +116,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Typical monthly gas usage", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_usage, @@ -131,7 +125,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill gas cost to date", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -141,7 +134,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Current bill gas forecasted cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -151,7 +143,6 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( name="Typical monthly gas cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index f892114b26c..05d301b5250 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import logging from hole import Hole @@ -28,13 +29,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - CONF_STATISTICS_ONLY, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN, - MIN_TIME_BETWEEN_UPDATES, -) +from .const import CONF_STATISTICS_ONLY, DOMAIN, MIN_TIME_BETWEEN_UPDATES _LOGGER = logging.getLogger(__name__) @@ -47,8 +42,18 @@ PLATFORMS = [ Platform.UPDATE, ] +PiHoleConfigEntry = ConfigEntry["PiHoleData"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class PiHoleData: + """Runtime data definition.""" + + api: Hole + coordinator: DataUpdateCoordinator[None] + + +async def async_setup_entry(hass: HomeAssistant, entry: PiHoleConfigEntry) -> bool: """Set up Pi-hole entry.""" name = entry.data[CONF_NAME] host = entry.data[CONF_HOST] @@ -126,11 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_KEY_API: api, - DATA_KEY_COORDINATOR: coordinator, - } + entry.runtime_data = PiHoleData(api, coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -139,19 +140,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Pi-hole entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -class PiHoleEntity(CoordinatorEntity): +class PiHoleEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Representation of a Pi-hole entity.""" def __init__( self, api: Hole, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[None], name: str, server_unique_id: str, ) -> None: diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 0593d12faa7..001a2ebcee8 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -12,14 +12,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PiHoleEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN +from . import PiHoleConfigEntry, PiHoleEntity @dataclass(frozen=True, kw_only=True) @@ -40,16 +38,18 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: PiHoleConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Pi-hole binary sensor.""" name = entry.data[CONF_NAME] - hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] + hole_data = entry.runtime_data binary_sensors = [ PiHoleBinarySensor( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], + hole_data.api, + hole_data.coordinator, name, entry.entry_id, description, @@ -69,7 +69,7 @@ class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity): def __init__( self, api: Hole, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[None], name: str, server_unique_id: str, description: PiHoleBinarySensorEntityDescription, diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index b6c97bc6118..c81e6504dff 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -17,6 +17,3 @@ SERVICE_DISABLE = "disable" SERVICE_DISABLE_ATTR_DURATION = "duration" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -DATA_KEY_API = "api" -DATA_KEY_COORDINATOR = "coordinator" diff --git a/homeassistant/components/pi_hole/diagnostics.py b/homeassistant/components/pi_hole/diagnostics.py index 46efebaf475..115c04c8234 100644 --- a/homeassistant/components/pi_hole/diagnostics.py +++ b/homeassistant/components/pi_hole/diagnostics.py @@ -4,23 +4,20 @@ from __future__ import annotations from typing import Any -from hole import Hole - from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from .const import DATA_KEY_API, DOMAIN +from . import PiHoleConfigEntry TO_REDACT = {CONF_API_KEY} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: PiHoleConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - api: Hole = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + api = entry.runtime_data.api return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index a62252d10c1..14ad3ac82dd 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -5,15 +5,13 @@ from __future__ import annotations from hole import Hole from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PiHoleEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN +from . import PiHoleConfigEntry, PiHoleEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -65,15 +63,17 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: PiHoleConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Pi-hole sensor.""" name = entry.data[CONF_NAME] - hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] + hole_data = entry.runtime_data sensors = [ PiHoleSensor( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], + hole_data.api, + hole_data.coordinator, name, entry.entry_id, description, @@ -92,7 +92,7 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): def __init__( self, api: Hole, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[None], name: str, server_unique_id: str, description: SensorEntityDescription, diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index 963ee7c9738..83ed3e6d787 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -9,34 +9,29 @@ from hole.exceptions import HoleError import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import PiHoleEntity -from .const import ( - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN as PIHOLE_DOMAIN, - SERVICE_DISABLE, - SERVICE_DISABLE_ATTR_DURATION, -) +from . import PiHoleConfigEntry, PiHoleEntity +from .const import SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: PiHoleConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Pi-hole switch.""" name = entry.data[CONF_NAME] - hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] + hole_data = entry.runtime_data switches = [ PiHoleSwitch( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], + hole_data.api, + hole_data.coordinator, name, entry.entry_id, ) diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py index 75d4f91f2be..db78d3ab0a5 100644 --- a/homeassistant/components/pi_hole/update.py +++ b/homeassistant/components/pi_hole/update.py @@ -8,14 +8,12 @@ from dataclasses import dataclass from hole import Hole from homeassistant.components.update import UpdateEntity, UpdateEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PiHoleEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from . import PiHoleConfigEntry, PiHoleEntity @dataclass(frozen=True) @@ -60,16 +58,18 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: PiHoleConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Pi-hole update entities.""" name = entry.data[CONF_NAME] - hole_data = hass.data[DOMAIN][entry.entry_id] + hole_data = entry.runtime_data async_add_entities( PiHoleUpdateEntity( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], + hole_data.api, + hole_data.coordinator, name, entry.entry_id, description, @@ -87,7 +87,7 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity): def __init__( self, api: Hole, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[None], name: str, server_unique_id: str, description: PiHoleUpdateEntityDescription, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 41cf4e22b53..572731a9fed 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -485,6 +485,12 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) - The actual calculation is delegated to the platforms. """ + # Define modified_statistic_ids outside of the "with" statement as + # _compile_statistics may raise and be trapped by + # filter_unique_constraint_integrity_error which would make + # modified_statistic_ids unbound. + modified_statistic_ids: set[str] | None = None + # Return if we already have 5-minute statistics for the requested period with session_scope( session=instance.get_session(), diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 9dcb2f9f57e..538bd2475dd 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -54,6 +54,8 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +SamsungTVConfigEntry = ConfigEntry[SamsungTVBridge] + @callback def _async_get_device_bridge( @@ -123,10 +125,8 @@ async def _async_update_ssdp_locations(hass: HomeAssistant, entry: ConfigEntry) hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) -> bool: """Set up the Samsung TV platform.""" - hass.data.setdefault(DOMAIN, {}) - # Initialize bridge if entry.data.get(CONF_METHOD) == METHOD_ENCRYPTED_WEBSOCKET: if not entry.data.get(CONF_TOKEN) or not entry.data.get(CONF_SESSION_ID): @@ -161,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(debounced_reloader.async_shutdown) entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) - hass.data[DOMAIN][entry.entry_id] = bridge + entry.runtime_data = bridge await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -250,11 +250,11 @@ async def _async_create_bridge_with_updated_data( return bridge -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] + bridge = entry.runtime_data LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) await bridge.async_close_remote() return unload_ok diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index 5b8ff3ebdb8..0e5c6608a17 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -15,7 +15,6 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger -from .const import DOMAIN from .helpers import ( async_get_client_by_device_entry, async_get_device_entry_by_device_id, @@ -43,8 +42,7 @@ async def async_validate_trigger_config( device_id = config[CONF_DEVICE_ID] try: device = async_get_device_entry_by_device_id(hass, device_id) - if DOMAIN in hass.data: - async_get_client_by_device_entry(hass, device) + async_get_client_by_device_entry(hass, device) except ValueError as err: raise InvalidDeviceAutomationConfig(err) from err diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index 5ce0c0393ca..a0da9a59261 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -5,21 +5,20 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from .bridge import SamsungTVBridge -from .const import CONF_SESSION_ID, DOMAIN +from . import SamsungTVConfigEntry +from .const import CONF_SESSION_ID TO_REDACT = {CONF_TOKEN, CONF_SESSION_ID} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: SamsungTVConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] + bridge = entry.runtime_data return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": await bridge.async_device_info(), diff --git a/homeassistant/components/samsungtv/helpers.py b/homeassistant/components/samsungtv/helpers.py index b334c60442b..f7d49f5e8cc 100644 --- a/homeassistant/components/samsungtv/helpers.py +++ b/homeassistant/components/samsungtv/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry @@ -52,10 +53,15 @@ def async_get_client_by_device_entry( Raises ValueError if client is not found. """ - domain_data: dict[str, SamsungTVBridge] = hass.data[DOMAIN] for config_entry_id in device.config_entries: - if bridge := domain_data.get(config_entry_id): - return bridge + entry = hass.config_entries.async_get_entry(config_entry_id) + if ( + entry + and entry.state == ConfigEntryState.LOADED + and hasattr(entry, "runtime_data") + and isinstance(entry.runtime_data, SamsungTVBridge) + ): + return entry.runtime_data raise ValueError( f"Device {device.id} is not from an existing {DOMAIN} config entry" diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index ff347431a4a..f227684c016 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -38,6 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger import PluggableAction from homeassistant.util.async_ import create_eager_task +from . import SamsungTVConfigEntry from .bridge import SamsungTVBridge, SamsungTVWSBridge from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, LOGGER from .entity import SamsungTVEntity @@ -65,10 +66,12 @@ APP_LIST_DELAY = 3 async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SamsungTVConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Samsung TV from a config entry.""" - bridge = hass.data[DOMAIN][entry.entry_id] + bridge = entry.runtime_data async_add_entities([SamsungTVDevice(bridge, entry)], True) diff --git a/homeassistant/components/samsungtv/remote.py b/homeassistant/components/samsungtv/remote.py index 752c5e2f950..c65bf17240b 100644 --- a/homeassistant/components/samsungtv/remote.py +++ b/homeassistant/components/samsungtv/remote.py @@ -6,19 +6,21 @@ from collections.abc import Iterable from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from . import SamsungTVConfigEntry +from .const import LOGGER from .entity import SamsungTVEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SamsungTVConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Samsung TV from a config entry.""" - bridge = hass.data[DOMAIN][entry.entry_id] + bridge = entry.runtime_data async_add_entities([SamsungTVRemote(bridge=bridge, config_entry=entry)]) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cfeab531687..2c6a2e4caad 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -16,7 +16,6 @@ from aioshelly.exceptions import ( from aioshelly.rpc_device import RpcDevice import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -35,7 +34,6 @@ from .const import ( BLOCK_WRONG_SLEEP_PERIOD, CONF_COAP_PORT, CONF_SLEEP_PERIOD, - DATA_CONFIG_ENTRY, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID, LOGGER, @@ -44,11 +42,11 @@ from .const import ( ) from .coordinator import ( ShellyBlockCoordinator, + ShellyConfigEntry, ShellyEntryData, ShellyRestCoordinator, ShellyRpcCoordinator, ShellyRpcPollingCoordinator, - get_entry_data, ) from .utils import ( async_create_issue_unsupported_firmware, @@ -102,15 +100,13 @@ CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Shelly component.""" - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - if (conf := config.get(DOMAIN)) is not None: - hass.data[DOMAIN][CONF_COAP_PORT] = conf[CONF_COAP_PORT] + hass.data[DOMAIN] = {CONF_COAP_PORT: conf[CONF_COAP_PORT]} return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool: """Set up Shelly from a config entry.""" # The custom component for Shelly devices uses shelly domain as well as core # integration. If the user removes the custom component but doesn't remove the @@ -127,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - get_entry_data(hass)[entry.entry_id] = ShellyEntryData() + entry.runtime_data = ShellyEntryData() if get_device_entry_gen(entry) in RPC_GENERATIONS: return await _async_setup_rpc_entry(hass, entry) @@ -135,7 +131,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await _async_setup_block_entry(hass, entry) -async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def _async_setup_block_entry( + hass: HomeAssistant, entry: ShellyConfigEntry +) -> bool: """Set up Shelly block based device from a config entry.""" options = ConnectionOptions( entry.data[CONF_HOST], @@ -163,7 +161,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) - shelly_entry_data = get_entry_data(hass)[entry.entry_id] + shelly_entry_data = entry.runtime_data # Some old firmware have a wrong sleep period hardcoded value. # Following code block will force the right value for affected devices @@ -220,7 +218,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b return True -async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool: """Set up Shelly RPC based device from a config entry.""" options = ConnectionOptions( entry.data[CONF_HOST], @@ -249,7 +247,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) - shelly_entry_data = get_entry_data(hass)[entry.entry_id] + shelly_entry_data = entry.runtime_data if sleep_period == 0: # Not a sleeping device, finish setup @@ -290,9 +288,9 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool: """Unload a config entry.""" - shelly_entry_data = get_entry_data(hass)[entry.entry_id] + shelly_entry_data = entry.runtime_data platforms = RPC_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): @@ -310,7 +308,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # and if we setup again, we will fix anything that is # in an inconsistent state at that time. await shelly_entry_data.rpc.shutdown() - get_entry_data(hass).pop(entry.entry_id) return unload_ok @@ -331,6 +328,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): if shelly_entry_data.block: shelly_entry_data.block.shutdown() - get_entry_data(hass).pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 04df9fb1adc..bdbf5904b15 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -12,13 +12,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import CONF_SLEEP_PERIOD +from .coordinator import ShellyConfigEntry from .entity import ( BlockEntityDescription, RestEntityDescription, @@ -220,7 +220,7 @@ RPC_SENSORS: Final = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 12c347908fb..8c1b1c4ef43 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -14,7 +14,6 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -24,7 +23,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify from .const import LOGGER, SHELLY_GAS_MODELS -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .utils import get_device_entry_gen _ShellyCoordinatorT = TypeVar( @@ -108,11 +107,11 @@ def async_migrate_unique_ids( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set buttons for device.""" - entry_data = get_entry_data(hass)[config_entry.entry_id] + entry_data = config_entry.runtime_data coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None if get_device_entry_gen(config_entry) in RPC_GENERATIONS: coordinator = entry_data.rpc diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 81289bc1a9b..6a3f6605a8c 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -18,7 +18,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError @@ -42,7 +41,7 @@ from .const import ( RPC_THERMOSTAT_SETTINGS, SHTRV_01_TEMPERATURE_SETTINGS, ) -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -54,14 +53,14 @@ from .utils import ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate device.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: return async_setup_rpc_entry(hass, config_entry, async_add_entities) - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator if coordinator.device.initialized: async_setup_climate_entities(async_add_entities, coordinator) @@ -99,7 +98,7 @@ def async_setup_climate_entities( @callback def async_restore_climate_entities( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, coordinator: ShellyBlockCoordinator, ) -> None: @@ -121,11 +120,11 @@ def async_restore_climate_entities( @callback def async_setup_rpc_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator climate_key_ids = get_rpc_key_ids(coordinator.device.status, "thermostat") diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 2ac0416bb6c..70dc60c4ad9 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -31,7 +31,6 @@ DOMAIN: Final = "shelly" LOGGER: Logger = getLogger(__package__) -DATA_CONFIG_ENTRY: Final = "config_entry" CONF_COAP_PORT: Final = "coap_port" FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index e321f393ba3..9ca0d19c574 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -39,7 +39,6 @@ from .const import ( BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, CONF_BLE_SCANNER_MODE, CONF_SLEEP_PERIOD, - DATA_CONFIG_ENTRY, DOMAIN, DUAL_MODE_LIGHT_MODELS, ENTRY_RELOAD_COOLDOWN, @@ -85,9 +84,7 @@ class ShellyEntryData: rpc_poll: ShellyRpcPollingCoordinator | None = None -def get_entry_data(hass: HomeAssistant) -> dict[str, ShellyEntryData]: - """Return Shelly entry data for a given config entry.""" - return cast(dict[str, ShellyEntryData], hass.data[DOMAIN][DATA_CONFIG_ENTRY]) +ShellyConfigEntry = ConfigEntry[ShellyEntryData] class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): @@ -96,7 +93,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ShellyConfigEntry, device: _DeviceT, update_interval: float, ) -> None: @@ -217,7 +214,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Coordinator for a Shelly block based device.""" def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + self, hass: HomeAssistant, entry: ShellyConfigEntry, device: BlockDevice ) -> None: """Initialize the Shelly block device coordinator.""" self.entry = entry @@ -424,7 +421,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): """Coordinator for a Shelly REST device.""" def __init__( - self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry + self, hass: HomeAssistant, device: BlockDevice, entry: ShellyConfigEntry ) -> None: """Initialize the Shelly REST device coordinator.""" update_interval = REST_SENSORS_UPDATE_INTERVAL @@ -458,7 +455,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Coordinator for a Shelly RPC based device.""" def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice + self, hass: HomeAssistant, entry: ShellyConfigEntry, device: RpcDevice ) -> None: """Initialize the Shelly RPC device coordinator.""" self.entry = entry @@ -538,7 +535,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): return _unsubscribe async def _async_update_listener( - self, hass: HomeAssistant, entry: ConfigEntry + self, hass: HomeAssistant, entry: ShellyConfigEntry ) -> None: """Reconfigure on update.""" async with self._connection_lock: @@ -721,7 +718,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): """Polling coordinator for a Shelly RPC based device.""" def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice + self, hass: HomeAssistant, entry: ShellyConfigEntry, device: RpcDevice ) -> None: """Initialize the RPC polling coordinator.""" super().__init__(hass, entry, device, RPC_SENSORS_POLLING_INTERVAL) @@ -747,10 +744,13 @@ def get_block_coordinator_by_device_id( dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: - if not (entry_data := get_entry_data(hass).get(config_entry)): - continue - - if coordinator := entry_data.block: + entry = hass.config_entries.async_get_entry(config_entry) + if ( + entry + and entry.state == ConfigEntryState.LOADED + and isinstance(entry.runtime_data, ShellyEntryData) + and (coordinator := entry.runtime_data.block) + ): return coordinator return None @@ -763,23 +763,25 @@ def get_rpc_coordinator_by_device_id( dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: - if not (entry_data := get_entry_data(hass).get(config_entry)): - continue - - if coordinator := entry_data.rpc: + entry = hass.config_entries.async_get_entry(config_entry) + if ( + entry + and entry.state == ConfigEntryState.LOADED + and isinstance(entry.runtime_data, ShellyEntryData) + and (coordinator := entry.runtime_data.rpc) + ): return coordinator return None -async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_reconnect_soon(hass: HomeAssistant, entry: ShellyConfigEntry) -> None: """Try to reconnect soon.""" if ( not entry.data.get(CONF_SLEEP_PERIOD) and not hass.is_stopping and entry.state == ConfigEntryState.LOADED - and (entry_data := get_entry_data(hass).get(entry.entry_id)) - and (coordinator := entry_data.rpc) + and (coordinator := entry.runtime_data.rpc) ): entry.async_create_background_task( hass, diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 2327c5b4779..395df95735b 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -13,18 +13,17 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import get_device_entry_gen, get_rpc_key_ids async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up covers for device.""" @@ -37,11 +36,11 @@ async def async_setup_entry( @callback def async_setup_block_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover for device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator and coordinator.device.blocks blocks = [block for block in coordinator.device.blocks if block.type == "roller"] @@ -54,11 +53,11 @@ def async_setup_block_entry( @callback def async_setup_rpc_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator cover_key_ids = get_rpc_key_ids(coordinator.device.status, "cover") diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index 473bef21835..db69abc8f55 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -6,21 +6,20 @@ from typing import Any from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -from .coordinator import get_entry_data +from .coordinator import ShellyConfigEntry TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: ShellyConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - shelly_entry_data = get_entry_data(hass)[entry.entry_id] + shelly_entry_data = entry.runtime_data device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 150244e2e47..b9f48bfd24d 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,7 +9,6 @@ from typing import Any, cast from aioshelly.block_device import Block from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo @@ -24,7 +23,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SLEEP_PERIOD, LOGGER -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .utils import ( async_remove_shelly_entity, get_block_entity_name, @@ -36,13 +35,13 @@ from .utils import ( @callback def async_setup_entry_attribute_entities( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for attributes.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator if coordinator.device.initialized: async_setup_block_attribute_entities( @@ -104,7 +103,7 @@ def async_setup_block_attribute_entities( @callback def async_restore_block_attribute_entities( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, coordinator: ShellyBlockCoordinator, sensors: Mapping[tuple[str, str], BlockEntityDescription], @@ -139,13 +138,13 @@ def async_restore_block_attribute_entities( @callback def async_setup_entry_rpc( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, sensors: Mapping[str, RpcEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for RPC sensors.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator if coordinator.device.initialized: @@ -161,18 +160,18 @@ def async_setup_entry_rpc( @callback def async_setup_rpc_attribute_entities( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, sensors: Mapping[str, RpcEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for RPC attributes.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator polling_coordinator = None if not (sleep_period := config_entry.data[CONF_SLEEP_PERIOD]): - polling_coordinator = get_entry_data(hass)[config_entry.entry_id].rpc_poll + polling_coordinator = config_entry.runtime_data.rpc_poll assert polling_coordinator entities = [] @@ -213,7 +212,7 @@ def async_setup_rpc_attribute_entities( @callback def async_restore_rpc_attribute_entities( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, coordinator: ShellyRpcCoordinator, sensors: Mapping[str, RpcEntityDescription], @@ -248,13 +247,13 @@ def async_restore_rpc_attribute_entities( @callback def async_setup_entry_rest( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, sensors: Mapping[str, RestEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for REST sensors.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rest + coordinator = config_entry.runtime_data.rest assert coordinator async_add_entities( diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index 0b6b81461ac..372d73dea3c 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -15,7 +15,6 @@ from homeassistant.components.event import ( EventEntity, EventEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,7 +25,7 @@ from .const import ( RPC_INPUTS_EVENTS_TYPES, SHIX3_1_INPUTS_EVENTS_TYPES, ) -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ShellyBlockEntity from .utils import ( async_remove_shelly_entity, @@ -73,7 +72,7 @@ RPC_EVENT: Final = ShellyRpcEventDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" @@ -82,7 +81,7 @@ async def async_setup_entry( coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc if TYPE_CHECKING: assert coordinator @@ -97,7 +96,7 @@ async def async_setup_entry( else: entities.append(ShellyRpcEvent(coordinator, key, RPC_EVENT)) else: - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block if TYPE_CHECKING: assert coordinator assert coordinator.device.blocks diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 0650e2d15e5..24231fbb33a 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -20,7 +20,6 @@ from homeassistant.components.light import ( LightEntityFeature, brightness_supported, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,7 +37,7 @@ from .const import ( SHELLY_PLUS_RGBW_CHANNELS, STANDARD_RGB_EFFECTS, ) -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -54,7 +53,7 @@ from .utils import ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up lights for device.""" @@ -67,11 +66,11 @@ async def async_setup_entry( @callback def async_setup_block_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator blocks = [] assert coordinator.device.blocks @@ -97,11 +96,11 @@ def async_setup_block_entry( @callback def async_setup_rpc_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 6fdf05fa9cb..f7630ef09b3 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -14,7 +14,6 @@ from homeassistant.components.number import ( NumberMode, RestoreNumber, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -22,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from .const import CONF_SLEEP_PERIOD, LOGGER -from .coordinator import ShellyBlockCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry from .entity import ( BlockEntityDescription, ShellySleepingBlockAttributeEntity, @@ -58,7 +57,7 @@ NUMBERS: dict[tuple[str, str], BlockNumberDescription] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up numbers for device.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 6cdeea9f842..7dea45c0c1f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( SensorExtraStoredData, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, @@ -38,7 +37,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, RestEntityDescription, @@ -995,7 +994,7 @@ RPC_SENSORS: Final = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for device.""" diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 81b16d48ab8..70b6754608b 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -22,13 +22,12 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN, GAS_VALVE_OPEN_STATES -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, ShellyBlockAttributeEntity, @@ -64,7 +63,7 @@ GAS_VALVE_SWITCH = BlockSwitchDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches for device.""" @@ -77,11 +76,11 @@ async def async_setup_entry( @callback def async_setup_block_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator # Add Shelly Gas Valve as a switch @@ -127,11 +126,11 @@ def async_setup_block_entry( @callback def async_setup_rpc_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = config_entry.runtime_data.rpc assert coordinator switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index dc6e9c9698a..a9673187408 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -18,7 +18,6 @@ from homeassistant.components.update import ( UpdateEntityDescription, UpdateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -26,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import CONF_SLEEP_PERIOD, OTA_BEGIN, OTA_ERROR, OTA_PROGRESS, OTA_SUCCESS -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( RestEntityDescription, RpcEntityDescription, @@ -103,7 +102,7 @@ RPC_UPDATES: Final = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up update entities for Shelly component.""" diff --git a/homeassistant/components/shelly/valve.py b/homeassistant/components/shelly/valve.py index a17738e3575..83c1f577439 100644 --- a/homeassistant/components/shelly/valve.py +++ b/homeassistant/components/shelly/valve.py @@ -14,11 +14,10 @@ from homeassistant.components.valve import ( ValveEntityDescription, ValveEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .coordinator import ShellyBlockCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry from .entity import ( BlockEntityDescription, ShellyBlockAttributeEntity, @@ -42,7 +41,7 @@ GAS_VALVE = BlockValveDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up valves for device.""" @@ -53,11 +52,11 @@ async def async_setup_entry( @callback def async_setup_block_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ShellyConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up valve for device.""" - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = config_entry.runtime_data.block assert coordinator and coordinator.device.blocks if coordinator.model == MODEL_GAS: diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 3c15f2fb820..19525ad9bfa 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -12,13 +12,16 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.start import async_at_started -from .const import DOMAIN from .coordinator import SpeedTestDataCoordinator PLATFORMS = [Platform.SENSOR] +SpeedTestConfigEntry = ConfigEntry[SpeedTestDataCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: SpeedTestConfigEntry +) -> bool: """Set up the Speedtest.net component.""" try: api = await hass.async_add_executor_job( @@ -28,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except speedtest.SpeedtestException as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN] = coordinator + config_entry.runtime_data = coordinator async def _async_finish_startup(hass: HomeAssistant) -> None: """Run this only when HA has finished its startup.""" @@ -45,11 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload SpeedTest Entry from config_entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ): - hass.data.pop(DOMAIN) - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 2ef2a70d745..dc64448bbef 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -6,14 +6,10 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.core import callback +from . import SpeedTestConfigEntry from .const import ( CONF_SERVER_ID, CONF_SERVER_NAME, @@ -31,7 +27,7 @@ class SpeedTestFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: SpeedTestConfigEntry, ) -> SpeedTestOptionsFlowHandler: """Get the options flow for this handler.""" return SpeedTestOptionsFlowHandler(config_entry) @@ -52,7 +48,7 @@ class SpeedTestFlowHandler(ConfigFlow, domain=DOMAIN): class SpeedTestOptionsFlowHandler(OptionsFlow): """Handle SpeedTest options.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, config_entry: SpeedTestConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._servers: dict = {} @@ -73,7 +69,7 @@ class SpeedTestOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title="", data=user_input) - self._servers = self.hass.data[DOMAIN].servers + self._servers = self.config_entry.runtime_data.servers options = { vol.Optional( diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 5bf1a6bea91..10da1dc93af 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -20,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SpeedTestConfigEntry from .const import ( ATTR_BYTES_RECEIVED, ATTR_BYTES_SENT, @@ -69,11 +69,11 @@ SENSOR_TYPES: tuple[SpeedtestSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: SpeedTestConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Speedtestdotnet sensors.""" - speedtest_coordinator = hass.data[DOMAIN] + speedtest_coordinator = config_entry.runtime_data async_add_entities( SpeedtestSensor(speedtest_coordinator, description) for description in SENSOR_TYPES diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 6308594f4bd..8f6f3098ee8 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -19,7 +19,7 @@ from .const import ( # noqa: F401 # noqa: F401 STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON, ) -from .entity import Sun +from .entity import Sun, SunConfigEntry CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -40,19 +40,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool: """Set up from a config entry.""" - hass.data[DOMAIN] = Sun(hass) + entry.runtime_data = sun = Sun(hass) + entry.async_on_unload(sun.remove_listeners) await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms( entry, [Platform.SENSOR] ): - sun: Sun = hass.data.pop(DOMAIN) - sun.remove_listeners() + sun = entry.runtime_data hass.states.async_remove(sun.entity_id) return unload_ok diff --git a/homeassistant/components/sun/entity.py b/homeassistant/components/sun/entity.py index 739784697e0..291f56718a3 100644 --- a/homeassistant/components/sun/entity.py +++ b/homeassistant/components/sun/entity.py @@ -8,6 +8,7 @@ from typing import Any from astral.location import Elevation, Location +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, SUN_EVENT_SUNRISE, @@ -30,6 +31,8 @@ from .const import ( STATE_BELOW_HORIZON, ) +SunConfigEntry = ConfigEntry["Sun"] + _LOGGER = logging.getLogger(__name__) ENTITY_ID = "sun.sun" diff --git a/homeassistant/components/sun/sensor.py b/homeassistant/components/sun/sensor.py index 018ba4fa994..e7e621d06cd 100644 --- a/homeassistant/components/sun/sensor.py +++ b/homeassistant/components/sun/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEGREE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -22,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED -from .entity import Sun +from .entity import Sun, SunConfigEntry ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".sun_{}" @@ -107,11 +106,11 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: SunConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Sun sensor platform.""" - sun: Sun = hass.data[DOMAIN] + sun = entry.runtime_data async_add_entities( [SunSensor(sun, description, entry.entry_id) for description in SENSOR_TYPES] diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2748b27c93d..d42dacca638 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -7,6 +7,7 @@ import logging from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station.camera import SynoCamera +from synology_dsm.exceptions import SynologyDSMNotLoggedInException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL @@ -69,7 +70,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await api.async_setup() except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: raise_config_entry_auth_error(err) - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + except (*SYNOLOGY_CONNECTION_EXCEPTIONS, SynologyDSMNotLoggedInException) as err: + # SynologyDSMNotLoggedInException may be raised even if the user is + # logged in because the session may have expired, and we need to retry + # the login later. if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: @@ -86,12 +90,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api) - await coordinator_central.async_config_entry_first_refresh() available_apis = api.dsm.apis - # The central coordinator needs to be refreshed first since - # the next two rely on data from it coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None = None if api.surveillance_station is not None: coordinator_cameras = SynologyDSMCameraUpdateCoordinator(hass, entry, api) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 04e8ae29ceb..91c4cfc4ae2 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from contextlib import suppress import logging @@ -38,6 +39,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_DEVICE_TOKEN, + DEFAULT_TIMEOUT, EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, SYNOLOGY_CONNECTION_EXCEPTIONS, @@ -82,6 +84,31 @@ class SynoApi: self._with_upgrade = True self._with_utilisation = True + self._login_future: asyncio.Future[None] | None = None + + async def async_login(self) -> None: + """Login to the Synology DSM API. + + This function will only login once if called multiple times + by multiple different callers. + + If a login is already in progress, the function will await the + login to complete before returning. + """ + if self._login_future: + return await self._login_future + + self._login_future = self._hass.loop.create_future() + try: + await self.dsm.login() + self._login_future.set_result(None) + except BaseException as err: + if not self._login_future.done(): + self._login_future.set_exception(err) + raise + finally: + self._login_future = None + async def async_setup(self) -> None: """Start interacting with the NAS.""" session = async_get_clientsession(self._hass, self._entry.data[CONF_VERIFY_SSL]) @@ -92,10 +119,10 @@ class SynoApi: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT) or 10, + timeout=self._entry.options.get(CONF_TIMEOUT) or DEFAULT_TIMEOUT, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) - await self.dsm.login() + await self.async_login() # check if surveillance station is used self._with_surveillance_station = bool( diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 785baa50b29..d6c0c6fe3e8 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -179,7 +179,9 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): port = DEFAULT_PORT session = async_get_clientsession(self.hass, verify_ssl) - api = SynologyDSM(session, host, port, username, password, use_ssl, timeout=30) + api = SynologyDSM( + session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT + ) errors = {} try: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 140e07e975b..35d3008b416 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -40,7 +40,7 @@ DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min -DEFAULT_TIMEOUT = 10 # sec +DEFAULT_TIMEOUT = 30 # sec DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 34886828a58..52a3e1de1eb 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import ( @@ -30,6 +31,36 @@ _LOGGER = logging.getLogger(__name__) _DataT = TypeVar("_DataT") +_T = TypeVar("_T", bound="SynologyDSMUpdateCoordinator") +_P = ParamSpec("_P") + + +def async_re_login_on_expired( + func: Callable[Concatenate[_T, _P], Awaitable[_DataT]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _DataT]]: + """Define a wrapper to re-login when expired.""" + + async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _DataT: + for attempts in range(2): + try: + return await func(self, *args, **kwargs) + except SynologyDSMNotLoggedInException: + # If login is expired, try to login again + _LOGGER.debug("login is expired, try to login again") + try: + await self.api.async_login() + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: + raise_config_entry_auth_error(err) + if attempts == 0: + continue + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + raise UpdateFailed("Unknown error when communicating with API") + + return _async_wrap + + class SynologyDSMUpdateCoordinator(DataUpdateCoordinator[_DataT]): """DataUpdateCoordinator base class for synology_dsm.""" @@ -72,6 +103,7 @@ class SynologyDSMSwitchUpdateCoordinator( assert info is not None self.version = info["data"]["CMSMinVersion"] + @async_re_login_on_expired async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station @@ -102,21 +134,10 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]): ), ) + @async_re_login_on_expired async def _async_update_data(self) -> None: """Fetch all data from api.""" - for attempts in range(2): - try: - await self.api.async_update() - except SynologyDSMNotLoggedInException: - # If login is expired, try to login again - try: - await self.api.dsm.login() - except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: - raise_config_entry_auth_error(err) - if attempts == 0: - continue - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + await self.api.async_update() class SynologyDSMCameraUpdateCoordinator( @@ -133,6 +154,7 @@ class SynologyDSMCameraUpdateCoordinator( """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) + @async_re_login_on_expired async def _async_update_data(self) -> dict[str, dict[int, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 25c131e547c..a0053fb4953 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -1,5 +1,6 @@ """The System Monitor integration.""" +from dataclasses import dataclass import logging import psutil_home_assistant as ha_psutil @@ -10,7 +11,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, DOMAIN_COORDINATOR from .coordinator import SystemMonitorCoordinator from .util import get_all_disk_mounts @@ -18,13 +18,26 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +SystemMonitorConfigEntry = ConfigEntry["SystemMonitorData"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class SystemMonitorData: + """Runtime data definition.""" + + coordinator: SystemMonitorCoordinator + psutil_wrapper: ha_psutil.PsutilWrapper + + +async def async_setup_entry( + hass: HomeAssistant, entry: SystemMonitorConfigEntry +) -> bool: """Set up System Monitor from a config entry.""" psutil_wrapper = await hass.async_add_executor_job(ha_psutil.PsutilWrapper) - hass.data[DOMAIN] = psutil_wrapper - disk_arguments = list(await hass.async_add_executor_job(get_all_disk_mounts, hass)) + disk_arguments = list( + await hass.async_add_executor_job(get_all_disk_mounts, hass, psutil_wrapper) + ) legacy_resources: set[str] = set(entry.options.get("resources", [])) for resource in legacy_resources: if resource.startswith("disk_"): @@ -40,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, psutil_wrapper, disk_arguments ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN_COORDINATOR] = coordinator + entry.runtime_data = SystemMonitorData(coordinator, psutil_wrapper) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/systemmonitor/binary_sensor.py b/homeassistant/components/systemmonitor/binary_sensor.py index 9efd6f3b4e0..157ec54920b 100644 --- a/homeassistant/components/systemmonitor/binary_sensor.py +++ b/homeassistant/components/systemmonitor/binary_sensor.py @@ -17,7 +17,6 @@ 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.device_registry import DeviceEntryType, DeviceInfo @@ -25,7 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATOR +from . import SystemMonitorConfigEntry +from .const import CONF_PROCESS, DOMAIN from .coordinator import SystemMonitorCoordinator _LOGGER = logging.getLogger(__name__) @@ -89,10 +89,12 @@ SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SystemMonitorConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up System Montor binary sensors based on a config entry.""" - coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] + coordinator = entry.runtime_data.coordinator async_add_entities( SystemMonitorSensor( diff --git a/homeassistant/components/systemmonitor/const.py b/homeassistant/components/systemmonitor/const.py index 4a6000323d5..798cb82f8ef 100644 --- a/homeassistant/components/systemmonitor/const.py +++ b/homeassistant/components/systemmonitor/const.py @@ -1,7 +1,6 @@ """Constants for System Monitor.""" DOMAIN = "systemmonitor" -DOMAIN_COORDINATOR = "systemmonitor_coordinator" CONF_INDEX = "index" CONF_PROCESS = "process" diff --git a/homeassistant/components/systemmonitor/diagnostics.py b/homeassistant/components/systemmonitor/diagnostics.py index 317758651d7..7a81f1598ea 100644 --- a/homeassistant/components/systemmonitor/diagnostics.py +++ b/homeassistant/components/systemmonitor/diagnostics.py @@ -4,18 +4,16 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN_COORDINATOR -from .coordinator import SystemMonitorCoordinator +from . import SystemMonitorConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: SystemMonitorConfigEntry ) -> dict[str, Any]: """Return diagnostics for Sensibo config entry.""" - coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] + coordinator = entry.runtime_data.coordinator diag_data = { "last_update_success": coordinator.last_update_success, diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e20f4703ab8..947f637c572 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -25,7 +25,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 ( CONF_RESOURCES, CONF_TYPE, @@ -47,7 +47,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATOR, NET_IO_TYPES +from . import SystemMonitorConfigEntry +from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES from .coordinator import SystemMonitorCoordinator from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature @@ -501,20 +502,23 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SystemMonitorConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up System Montor sensors based on a config entry.""" entities: list[SystemMonitorSensor] = [] legacy_resources: set[str] = set(entry.options.get("resources", [])) loaded_resources: set[str] = set() - coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] + coordinator = entry.runtime_data.coordinator + psutil_wrapper = entry.runtime_data.psutil_wrapper sensor_data = coordinator.data def get_arguments() -> dict[str, Any]: """Return startup information.""" return { - "disk_arguments": get_all_disk_mounts(hass), - "network_arguments": get_all_network_interfaces(hass), + "disk_arguments": get_all_disk_mounts(hass, psutil_wrapper), + "network_arguments": get_all_network_interfaces(hass, psutil_wrapper), } cpu_temperature: float | None = None diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 1889e443b2d..2a4b889bdde 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -8,16 +8,17 @@ import psutil_home_assistant as ha_psutil from homeassistant.core import HomeAssistant -from .const import CPU_SENSOR_PREFIXES, DOMAIN +from .const import CPU_SENSOR_PREFIXES _LOGGER = logging.getLogger(__name__) SKIP_DISK_TYPES = {"proc", "tmpfs", "devtmpfs"} -def get_all_disk_mounts(hass: HomeAssistant) -> set[str]: +def get_all_disk_mounts( + hass: HomeAssistant, psutil_wrapper: ha_psutil.PsutilWrapper +) -> set[str]: """Return all disk mount points on system.""" - psutil_wrapper: ha_psutil = hass.data[DOMAIN] disks: set[str] = set() for part in psutil_wrapper.psutil.disk_partitions(all=True): if os.name == "nt": @@ -53,9 +54,10 @@ def get_all_disk_mounts(hass: HomeAssistant) -> set[str]: return disks -def get_all_network_interfaces(hass: HomeAssistant) -> set[str]: +def get_all_network_interfaces( + hass: HomeAssistant, psutil_wrapper: ha_psutil.PsutilWrapper +) -> set[str]: """Return all network interfaces on system.""" - psutil_wrapper: ha_psutil = hass.data[DOMAIN] interfaces: set[str] = set() for interface in psutil_wrapper.psutil.net_if_addrs(): if interface.startswith("veth"): @@ -68,7 +70,7 @@ def get_all_network_interfaces(hass: HomeAssistant) -> set[str]: def get_all_running_processes(hass: HomeAssistant) -> set[str]: """Return all running processes on system.""" - psutil_wrapper: ha_psutil = hass.data.get(DOMAIN, ha_psutil.PsutilWrapper()) + psutil_wrapper = ha_psutil.PsutilWrapper() processes: set[str] = set() for proc in psutil_wrapper.psutil.process_iter(["name"]): if proc.name() not in processes: diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 76e0a09af39..bb19697b1e7 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,29 +1,20 @@ """The totalconnect component.""" -from datetime import timedelta -import logging - from total_connect_client.client import TotalConnectClient -from total_connect_client.exceptions import ( - AuthenticationError, - ServiceUnavailable, - TotalConnectError, -) +from total_connect_client.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN +from .coordinator import TotalConnectDataUpdateCoordinator PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -76,41 +67,3 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: client = hass.data[DOMAIN][entry.entry_id].client for location_id in client.locations: client.locations[location_id].auto_bypass_low_battery = bypass - - -class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module - """Class to fetch data from TotalConnect.""" - - config_entry: ConfigEntry - - def __init__(self, hass: HomeAssistant, client: TotalConnectClient) -> None: - """Initialize.""" - self.hass = hass - self.client = client - super().__init__( - hass, logger=_LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL - ) - - async def _async_update_data(self) -> None: - """Update data.""" - await self.hass.async_add_executor_job(self.sync_update_data) - - def sync_update_data(self) -> None: - """Fetch synchronous data from TotalConnect.""" - try: - for location_id in self.client.locations: - self.client.locations[location_id].get_panel_meta_data() - except AuthenticationError as exception: - # should only encounter if password changes during operation - raise ConfigEntryAuthFailed( - "TotalConnect authentication failed during operation." - ) from exception - except ServiceUnavailable as exception: - raise UpdateFailed( - "Error connecting to TotalConnect or the service is unavailable. " - "Check https://status.resideo.com/ for outages." - ) from exception - except TotalConnectError as exception: - raise UpdateFailed(exception) from exception - except ValueError as exception: - raise UpdateFailed("Unknown state from TotalConnect") from exception diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 1de9db1d319..511a0fd6270 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -26,8 +26,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .coordinator import TotalConnectDataUpdateCoordinator from .entity import TotalConnectLocationEntity SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant" diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 85461805124..62f84b3b69a 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -17,8 +17,8 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .coordinator import TotalConnectDataUpdateCoordinator from .entity import TotalConnectLocationEntity, TotalConnectZoneEntity LOW_BATTERY = "low_battery" diff --git a/homeassistant/components/totalconnect/button.py b/homeassistant/components/totalconnect/button.py index ec2d0a604c7..fc5b5e89587 100644 --- a/homeassistant/components/totalconnect/button.py +++ b/homeassistant/components/totalconnect/button.py @@ -12,8 +12,8 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .coordinator import TotalConnectDataUpdateCoordinator from .entity import TotalConnectLocationEntity, TotalConnectZoneEntity diff --git a/homeassistant/components/totalconnect/coordinator.py b/homeassistant/components/totalconnect/coordinator.py new file mode 100644 index 00000000000..9b500db1951 --- /dev/null +++ b/homeassistant/components/totalconnect/coordinator.py @@ -0,0 +1,58 @@ +"""The totalconnect component.""" + +from datetime import timedelta +import logging + +from total_connect_client.client import TotalConnectClient +from total_connect_client.exceptions import ( + AuthenticationError, + ServiceUnavailable, + TotalConnectError, +) + +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 DOMAIN + +SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) + + +class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Class to fetch data from TotalConnect.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, client: TotalConnectClient) -> None: + """Initialize.""" + self.client = client + super().__init__( + hass, logger=_LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL + ) + + async def _async_update_data(self) -> None: + """Update data.""" + await self.hass.async_add_executor_job(self.sync_update_data) + + def sync_update_data(self) -> None: + """Fetch synchronous data from TotalConnect.""" + try: + for location_id in self.client.locations: + self.client.locations[location_id].get_panel_meta_data() + except AuthenticationError as exception: + # should only encounter if password changes during operation + raise ConfigEntryAuthFailed( + "TotalConnect authentication failed during operation." + ) from exception + except ServiceUnavailable as exception: + raise UpdateFailed( + "Error connecting to TotalConnect or the service is unavailable. " + "Check https://status.resideo.com/ for outages." + ) from exception + except TotalConnectError as exception: + raise UpdateFailed(exception) from exception + except ValueError as exception: + raise UpdateFailed("Unknown state from TotalConnect") from exception diff --git a/homeassistant/components/totalconnect/entity.py b/homeassistant/components/totalconnect/entity.py index a18ffc14df5..e2b619ea500 100644 --- a/homeassistant/components/totalconnect/entity.py +++ b/homeassistant/components/totalconnect/entity.py @@ -6,7 +6,8 @@ from total_connect_client.zone import TotalConnectZone from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, TotalConnectDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import TotalConnectDataUpdateCoordinator class TotalConnectEntity(CoordinatorEntity[TotalConnectDataUpdateCoordinator]): diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index a8b23041a39..87ec14621d9 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/totalconnect", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], - "requirements": ["total-connect-client==2024.4"] + "requirements": ["total-connect-client==2024.5"] } diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 67c45c5dbdf..9cab66cab24 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -1,5 +1,7 @@ """The VLC media player Telnet integration.""" +from dataclasses import dataclass + from aiovlc.client import Client from aiovlc.exceptions import AuthError, ConnectError @@ -8,12 +10,22 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import DATA_AVAILABLE, DATA_VLC, DOMAIN, LOGGER +from .const import LOGGER PLATFORMS = [Platform.MEDIA_PLAYER] +VlcConfigEntry = ConfigEntry["VlcData"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class VlcData: + """Runtime data definition.""" + + vlc: Client + available: bool + + +async def async_setup_entry(hass: HomeAssistant, entry: VlcConfigEntry) -> bool: """Set up VLC media player Telnet from a config entry.""" config = entry.data @@ -31,15 +43,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.warning("Failed to connect to VLC: %s. Trying again", err) available = False + async def _disconnect_vlc() -> None: + """Disconnect from VLC.""" + LOGGER.debug("Disconnecting from VLC") + try: + await vlc.disconnect() + except ConnectError as err: + LOGGER.warning("Connection error: %s", err) + if available: try: await vlc.login() except AuthError as err: - await disconnect_vlc(vlc) + await _disconnect_vlc() raise ConfigEntryAuthFailed from err - domain_data = hass.data.setdefault(DOMAIN, {}) - domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available} + entry.runtime_data = VlcData(vlc, available) + + entry.async_on_unload(_disconnect_vlc) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -48,21 +69,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - entry_data = hass.data[DOMAIN].pop(entry.entry_id) - vlc = entry_data[DATA_VLC] - - await disconnect_vlc(vlc) - - return unload_ok - - -async def disconnect_vlc(vlc: Client) -> None: - """Disconnect from VLC.""" - LOGGER.debug("Disconnecting from VLC") - try: - await vlc.disconnect() - except ConnectError as err: - LOGGER.warning("Connection error: %s", err) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index fa021352d81..7d4b8490c77 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -25,7 +25,8 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DOMAIN, LOGGER +from . import VlcConfigEntry +from .const import DEFAULT_NAME, DOMAIN, LOGGER MAX_VOLUME = 500 @@ -34,13 +35,13 @@ _P = ParamSpec("_P") async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: VlcConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the vlc platform.""" # CONF_NAME is only present in imported YAML. name = entry.data.get(CONF_NAME) or DEFAULT_NAME - vlc = hass.data[DOMAIN][entry.entry_id][DATA_VLC] - available = hass.data[DOMAIN][entry.entry_id][DATA_AVAILABLE] + vlc = entry.runtime_data.vlc + available = entry.runtime_data.available async_add_entities([VlcDevice(entry, vlc, name, available)], True) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 95655f439c9..048e969b238 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -8,18 +8,9 @@ from contextlib import suppress from datetime import timedelta from functools import cached_property, partial import logging -from typing import ( - Any, - Final, - Generic, - Literal, - Required, - TypedDict, - TypeVar, - cast, - final, -) +from typing import Any, Final, Generic, Literal, Required, TypedDict, cast, final +from typing_extensions import TypeVar import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -137,21 +128,25 @@ LEGACY_SERVICE_GET_FORECAST: Final = "get_forecast" SERVICE_GET_FORECASTS: Final = "get_forecasts" _ObservationUpdateCoordinatorT = TypeVar( - "_ObservationUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" + "_ObservationUpdateCoordinatorT", + bound=DataUpdateCoordinator[Any], + default=DataUpdateCoordinator[dict[str, Any]], ) -# Note: -# Mypy bug https://github.com/python/mypy/issues/9424 prevents us from making the -# forecast cooordinators optional, bound=TimestampDataUpdateCoordinator[Any] | None - _DailyForecastUpdateCoordinatorT = TypeVar( - "_DailyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" + "_DailyForecastUpdateCoordinatorT", + bound=TimestampDataUpdateCoordinator[Any], + default=TimestampDataUpdateCoordinator[None], ) _HourlyForecastUpdateCoordinatorT = TypeVar( - "_HourlyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" + "_HourlyForecastUpdateCoordinatorT", + bound=TimestampDataUpdateCoordinator[Any], + default=_DailyForecastUpdateCoordinatorT, ) _TwiceDailyForecastUpdateCoordinatorT = TypeVar( - "_TwiceDailyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" + "_TwiceDailyForecastUpdateCoordinatorT", + bound=TimestampDataUpdateCoordinator[Any], + default=_DailyForecastUpdateCoordinatorT, ) # mypy: disallow-any-generics @@ -1089,8 +1084,8 @@ class CoordinatorWeatherEntity( *, context: Any = None, daily_coordinator: _DailyForecastUpdateCoordinatorT | None = None, - hourly_coordinator: _DailyForecastUpdateCoordinatorT | None = None, - twice_daily_coordinator: _DailyForecastUpdateCoordinatorT | None = None, + hourly_coordinator: _HourlyForecastUpdateCoordinatorT | None = None, + twice_daily_coordinator: _TwiceDailyForecastUpdateCoordinatorT | None = None, daily_forecast_valid: timedelta | None = None, hourly_forecast_valid: timedelta | None = None, twice_daily_forecast_valid: timedelta | None = None, @@ -1244,19 +1239,12 @@ class CoordinatorWeatherEntity( class SingleCoordinatorWeatherEntity( CoordinatorWeatherEntity[ - _ObservationUpdateCoordinatorT, - TimestampDataUpdateCoordinator[None], - TimestampDataUpdateCoordinator[None], - TimestampDataUpdateCoordinator[None], + _ObservationUpdateCoordinatorT, TimestampDataUpdateCoordinator[None] ], ): """A class for weather entities using a single DataUpdateCoordinators. - This class is added as a convenience because: - - Deriving from CoordinatorWeatherEntity requires specifying all type parameters - until we upgrade to Python 3.12 which supports defaults - - Mypy bug https://github.com/python/mypy/issues/9424 prevents us from making the - forecast cooordinator type vars optional + This class is added as a convenience. """ def __init__( diff --git a/homeassistant/components/wolflink/const.py b/homeassistant/components/wolflink/const.py index 59329ee41dd..b752b00790f 100644 --- a/homeassistant/components/wolflink/const.py +++ b/homeassistant/components/wolflink/const.py @@ -67,7 +67,7 @@ STATES = { "Kombigerät mit Solareinbindung": "kombigerat_mit_solareinbindung", "Heizgerät mit Speicher": "heizgerat_mit_speicher", "Nur Heizgerät": "nur_heizgerat", - "Aktiviert": "ktiviert", + "Aktiviert": "aktiviert", "Sparen": "sparen", "Estrichtrocknung": "estrichtrocknung", "Telefonfernschalter": "telefonfernschalter", diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f628879a7fd..0bd494992b6 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -352,6 +352,18 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): ) -> _FlowResultT: """Continue a data entry flow.""" result: _FlowResultT | None = None + + # Workaround for flow handlers which have not been upgraded to pass a show + # progress task, needed because of the change to eager tasks in HA Core 2024.5, + # can be removed in HA Core 2024.8. + flow = self._progress.get(flow_id) + if flow and flow.deprecated_show_progress: + if (cur_step := flow.cur_step) and cur_step[ + "type" + ] == FlowResultType.SHOW_PROGRESS: + # Allow the progress task to finish before we call the flow handler + await asyncio.sleep(0) + while not result or result["type"] == FlowResultType.SHOW_PROGRESS_DONE: result = await self._async_configure(flow_id, user_input) flow = self._progress.get(flow_id) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5cffe992c0d..5c026064c28 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1436,12 +1436,18 @@ class _TrackPointUTCTime: """Initialize track job.""" loop = self.hass.loop self._cancel_callback = loop.call_at( - loop.time() + self.expected_fire_timestamp - time.time(), self._run_action + loop.time() + self.expected_fire_timestamp - time.time(), self ) @callback - def _run_action(self) -> None: - """Call the action.""" + def __call__(self) -> None: + """Call the action. + + We implement this as __call__ so when debug logging logs the object + it shows the name of the job. This is especially helpful when asyncio + debug logging is enabled as we can see the name of the job that is + being called that is blocking the event loop. + """ # Depending on the available clock support (including timer hardware # and the OS kernel) it can happen that we fire a little bit too early # as measured by utcnow(). That is bad when callbacks have assumptions @@ -1450,7 +1456,7 @@ class _TrackPointUTCTime: if (delta := (self.expected_fire_timestamp - time_tracker_timestamp())) > 0: _LOGGER.debug("Called %f seconds too early, rearming", delta) loop = self.hass.loop - self._cancel_callback = loop.call_at(loop.time() + delta, self._run_action) + self._cancel_callback = loop.call_at(loop.time() + delta, self) return self.hass.async_run_hass_job(self.job, self.utc_point_in_time) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c4db601fac6..a45ba2d1129 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -98,6 +98,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: from homeassistant.components.light import LightEntityFeature from homeassistant.components.lock import LockEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature + from homeassistant.components.notify import NotifyEntityFeature from homeassistant.components.remote import RemoteEntityFeature from homeassistant.components.siren import SirenEntityFeature from homeassistant.components.todo import TodoListEntityFeature @@ -119,6 +120,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: "LightEntityFeature": LightEntityFeature, "LockEntityFeature": LockEntityFeature, "MediaPlayerEntityFeature": MediaPlayerEntityFeature, + "NotifyEntityFeature": NotifyEntityFeature, "RemoteEntityFeature": RemoteEntityFeature, "SirenEntityFeature": SirenEntityFeature, "TodoListEntityFeature": TodoListEntityFeature, diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1a72c8eb351..89c3442be6a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -90,7 +90,12 @@ class BlockedIntegration: BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { # Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464 - "start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant") + "start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant"), + # Added in 2024.5.1 because of + # https://community.home-assistant.io/t/psa-2024-5-upgrade-failure-and-dreame-vacuum-custom-integration/724612 + "dreame_vacuum": BlockedIntegration( + AwesomeVersion("1.0.4"), "crashes Home Assistant" + ), } DATA_COMPONENTS = "components" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1932f5f5e7f..7da3daf269c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-isal==0.2.0 +aiohttp-isal==0.3.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 @@ -17,7 +17,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 @@ -28,7 +28,7 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.8.1 +habluetooth==3.0.1 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 diff --git a/pyproject.toml b/pyproject.toml index d3f2af6bbf9..c036daeb35e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-isal==0.2.0", + "aiohttp-isal==0.3.1", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", @@ -659,7 +659,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.4.2" +required-version = ">=0.4.3" [tool.ruff.lint] select = [ diff --git a/requirements.txt b/requirements.txt index 44c60aec07a..df001251a04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-isal==0.2.0 +aiohttp-isal==0.3.1 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d2950403d4c..ea80e424896 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -437,7 +437,7 @@ amcrest==1.9.8 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.14 +androidtvremote2==0.0.15 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 @@ -579,7 +579,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 @@ -983,7 +983,7 @@ gotailwind==0.2.2 govee-ble==0.31.2 # homeassistant.components.govee_light_local -govee-local-api==1.4.4 +govee-local-api==1.4.5 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 @@ -1035,7 +1035,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.8.1 +habluetooth==3.0.1 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -1137,7 +1137,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.0 +imgw_pib==1.0.1 # homeassistant.components.incomfort incomfort-client==0.5.0 @@ -1371,7 +1371,7 @@ netdata==1.1.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.0.0 +nettigo-air-monitor==3.0.1 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -2004,7 +2004,7 @@ pynobo==1.8.1 pynuki==1.6.3 # homeassistant.components.nws -pynws==1.6.0 +pynws[retry]==1.7.0 # homeassistant.components.nx584 pynx584==0.5 @@ -2734,7 +2734,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2024.4 +total-connect-client==2024.5 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 065b4f63aa3..245387d3723 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ amberelectric==1.1.0 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.14 +androidtvremote2==0.0.15 # homeassistant.components.anova anova-wifi==0.10.0 @@ -494,7 +494,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 @@ -806,7 +806,7 @@ gotailwind==0.2.2 govee-ble==0.31.2 # homeassistant.components.govee_light_local -govee-local-api==1.4.4 +govee-local-api==1.4.5 # homeassistant.components.gpsd gps3==0.33.3 @@ -849,7 +849,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.8.1 +habluetooth==3.0.1 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -924,7 +924,7 @@ idasen-ha==2.5.1 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.0 +imgw_pib==1.0.1 # homeassistant.components.influxdb influxdb-client==1.24.0 @@ -1107,7 +1107,7 @@ nessclient==1.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.0.0 +nettigo-air-monitor==3.0.1 # homeassistant.components.nexia nexia==2.0.8 @@ -1567,7 +1567,7 @@ pynobo==1.8.1 pynuki==1.6.3 # homeassistant.components.nws -pynws==1.6.0 +pynws[retry]==1.7.0 # homeassistant.components.nx584 pynx584==0.5 @@ -2111,7 +2111,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2024.4 +total-connect-client==2024.5 # homeassistant.components.tplink_omada tplink-omada-client==1.3.12 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 05e98a945d2..de3776d7416 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.2.6 -ruff==0.4.2 +ruff==0.4.3 yamllint==1.35.1 diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 3622a21f633..45521903a08 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -97,6 +97,7 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=0, ) VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -141,6 +142,7 @@ VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=0, ) UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -161,6 +163,7 @@ UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=0, ) WAVE_DEVICE_INFO = AirthingsDevice( diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py index a6b32d56e4c..18bebfb44a4 100644 --- a/tests/components/aranet/__init__.py +++ b/tests/components/aranet/__init__.py @@ -31,6 +31,7 @@ def fake_service_info(name, service_uuid, manufacturer_data): tx_power=-127, platform_data=(), ), + tx_power=-127, ) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 18b86f561d0..c6f03f8bd64 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -561,11 +561,15 @@ async def test_ws_delete_all_refresh_tokens_error( "message": "During removal, an error was raised.", } - assert ( - "homeassistant.components.auth", - logging.ERROR, - "During refresh token removal, the following error occurred: I'm bad", - ) in caplog.record_tuples + records = [ + record + for record in caplog.records + if record.msg == "Error during refresh token removal" + ] + assert len(records) == 1 + assert records[0].levelno == logging.ERROR + assert records[0].exc_info and str(records[0].exc_info[1]) == "I'm bad" + assert records[0].name == "homeassistant.components.auth" for token in tokens: refresh_token = hass.auth.async_get_refresh_token(token["id"]) diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 5948874f0bf..11ef1ef1cdf 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -9,7 +9,6 @@ import pytest from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN -from homeassistant.components.axis.hub import AxisHub from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( @@ -52,7 +51,7 @@ async def test_device_setup( device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" - hub = AxisHub.get_hub(hass, setup_config_entry) + hub = setup_config_entry.runtime_data assert hub.api.vapix.firmware_version == "9.10.1" assert hub.api.vapix.product_number == "M1065-LW" @@ -78,7 +77,7 @@ async def test_device_setup( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) async def test_device_info(hass: HomeAssistant, setup_config_entry) -> None: """Verify other path of device information works.""" - hub = AxisHub.get_hub(hass, setup_config_entry) + hub = setup_config_entry.runtime_data assert hub.api.vapix.firmware_version == "9.80.1" assert hub.api.vapix.product_number == "M1065-LW" @@ -124,30 +123,26 @@ async def test_update_address( hass: HomeAssistant, setup_config_entry, mock_vapix_requests ) -> None: """Test update address works.""" - hub = AxisHub.get_hub(hass, setup_config_entry) + hub = setup_config_entry.runtime_data assert hub.api.config.host == "1.2.3.4" - with patch( - "homeassistant.components.axis.async_setup_entry", return_value=True - ) as mock_setup_entry: - mock_vapix_requests("2.3.4.5") - await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data=zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("2.3.4.5"), - ip_addresses=[ip_address("2.3.4.5")], - hostname="mock_hostname", - name="name", - port=80, - properties={"macaddress": MAC}, - type="mock_type", - ), - context={"source": SOURCE_ZEROCONF}, - ) - await hass.async_block_till_done() + mock_vapix_requests("2.3.4.5") + await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("2.3.4.5"), + ip_addresses=[ip_address("2.3.4.5")], + hostname="mock_hostname", + name="name", + port=80, + properties={"macaddress": MAC}, + type="mock_type", + ), + context={"source": SOURCE_ZEROCONF}, + ) + await hass.async_block_till_done() assert hub.api.config.host == "2.3.4.5" - assert len(mock_setup_entry.mock_calls) == 1 async def test_device_unavailable( diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 675f3de67ee..eae867b96d5 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -155,6 +155,7 @@ def inject_advertisement_with_time_and_source_connectable( advertisement=adv, connectable=connectable, time=time, + tx_power=adv.tx_power, ) ) diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index c67bd583b1e..462c43380a8 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -335,6 +335,7 @@ async def test_diagnostics_macos( "service_uuids": [], "source": "local", "time": ANY, + "tx_power": -127, } ], "connectable_history": [ @@ -363,6 +364,7 @@ async def test_diagnostics_macos( "service_uuids": [], "source": "local", "time": ANY, + "tx_power": -127, } ], "scanners": [ @@ -526,6 +528,7 @@ async def test_diagnostics_remote_adapter( "service_uuids": [], "source": "esp32", "time": ANY, + "tx_power": -127, } ], "connectable_history": [ @@ -554,6 +557,7 @@ async def test_diagnostics_remote_adapter( "service_uuids": [], "source": "esp32", "time": ANY, + "tx_power": -127, } ], "scanners": [ diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 3578e2e6f6f..047034bbf63 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -465,6 +465,7 @@ async def test_unavailable_after_no_data( device=MagicMock(), advertisement=MagicMock(), connectable=True, + tx_power=0, ) inject_bluetooth_service_info_bleak(hass, service_info_at_time) diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 627f2ffadcc..6346b094eab 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -92,6 +92,7 @@ async def test_do_not_see_device_if_time_not_updated( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name with time = 0 for all the updates mock_async_discovered_service_info.return_value = [device] @@ -157,6 +158,7 @@ async def test_see_device_if_time_updated( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name with time = 0 initially mock_async_discovered_service_info.return_value = [device] @@ -191,6 +193,7 @@ async def test_see_device_if_time_updated( advertisement=generate_advertisement_data(local_name="empty"), time=1, connectable=False, + tx_power=-127, ) # Return with name with time = 0 initially mock_async_discovered_service_info.return_value = [device] @@ -237,6 +240,7 @@ async def test_preserve_new_tracked_device_name( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -262,6 +266,7 @@ async def test_preserve_new_tracked_device_name( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -305,6 +310,7 @@ async def test_tracking_battery_times_out( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -373,6 +379,7 @@ async def test_tracking_battery_fails( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=False, + tx_power=-127, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -440,6 +447,7 @@ async def test_tracking_battery_successful( advertisement=generate_advertisement_data(local_name="empty"), time=0, connectable=True, + tx_power=-127, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index ae7231b8740..1f16dd8c6ac 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -18,6 +18,7 @@ TEMP_HUMI_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -36,6 +37,7 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) PRST_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -54,6 +56,7 @@ PRST_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="prst"), time=0, connectable=False, + tx_power=-127, ) INVALID_PAYLOAD = BluetoothServiceInfoBleak( @@ -70,6 +73,7 @@ INVALID_PAYLOAD = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -84,6 +88,7 @@ NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) @@ -103,6 +108,7 @@ def make_bthome_v1_adv(address: str, payload: bytes) -> BluetoothServiceInfoBlea advertisement=generate_advertisement_data(local_name="Test Device"), time=0, connectable=False, + tx_power=-127, ) @@ -124,6 +130,7 @@ def make_encrypted_bthome_v1_adv( advertisement=generate_advertisement_data(local_name="ATC 8F80A5"), time=0, connectable=False, + tx_power=-127, ) @@ -143,4 +150,5 @@ def make_bthome_v2_adv(address: str, payload: bytes) -> BluetoothServiceInfoBlea advertisement=generate_advertisement_data(local_name="Test Device"), time=0, connectable=False, + tx_power=-127, ) diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index b0536873d66..50730fb6c1e 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -69,7 +69,17 @@ async def test_sending_message(hass: HomeAssistant, events: list[Event]) -> None await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) await hass.async_block_till_done() last_event = events[-1] - assert last_event.data[notify.ATTR_MESSAGE] == "Test message" + assert last_event.data == {notify.ATTR_MESSAGE: "Test message"} + + data[notify.ATTR_TITLE] = "My title" + # Test with Title + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) + await hass.async_block_till_done() + last_event = events[-1] + assert last_event.data == { + notify.ATTR_MESSAGE: "Test message", + notify.ATTR_TITLE: "My title", + } async def test_calling_notify_from_script_loaded_from_yaml( diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 61a9f1c7d8c..422a24c3be0 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -257,9 +257,7 @@ class HomeControlMock(HomeControl): self.gateway = MagicMock() self.gateway.local_connection = True self.gateway.firmware_version = "8.94.0" - - def websocket_disconnect(self, event: str = "") -> None: - """Mock disconnect of the websocket.""" + self.websocket_disconnect = MagicMock() class HomeControlMockBinarySensor(HomeControlMock): diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index fa32d67d86c..9c3b1668991 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -6,8 +6,9 @@ from devolo_home_control_api.exceptions.gateway import GatewayOfflineError import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.devolo_home_control import DOMAIN +from homeassistant.components.devolo_home_control.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -63,6 +64,23 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED +async def test_home_assistant_stop(hass: HomeAssistant) -> None: + """Test home assistant stop.""" + entry = configure_integration(hass) + test_gateway = HomeControlMock() + test_gateway2 = HomeControlMock() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, test_gateway2], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert test_gateway.websocket_disconnect.called + assert test_gateway2.websocket_disconnect.called + + async def test_remove_device( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index d3ab3b831f0..913e33f6367 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from pydiscovergy.models import Reading import pytest -from homeassistant.components.discovergy import DOMAIN +from homeassistant.components.discovergy.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/dormakaba_dkey/__init__.py b/tests/components/dormakaba_dkey/__init__.py index be51109b2a1..b1301c6f048 100644 --- a/tests/components/dormakaba_dkey/__init__.py +++ b/tests/components/dormakaba_dkey/__init__.py @@ -18,6 +18,7 @@ DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak( ), time=0, connectable=True, + tx_power=-127, ) @@ -36,4 +37,5 @@ NOT_DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/drop_connect/common.py b/tests/components/drop_connect/common.py index 2e4d59fe7b2..bdba79bbd95 100644 --- a/tests/components/drop_connect/common.py +++ b/tests/components/drop_connect/common.py @@ -1,5 +1,7 @@ """Define common test values.""" +from syrupy import SnapshotAssertion + from homeassistant.components.drop_connect.const import ( CONF_COMMAND_TOPIC, CONF_DATA_TOPIC, @@ -12,6 +14,9 @@ from homeassistant.components.drop_connect.const import ( DOMAIN, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -216,3 +221,27 @@ def config_entry_ro_filter() -> ConfigEntry: }, version=1, ) + + +def help_assert_entries( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry: ConfigEntry, + step: str, + assert_unknown: bool = False, +) -> None: + """Assert platform entities and state.""" + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert entity_entries + if assert_unknown: + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id).state == STATE_UNKNOWN + return + + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-{step}" + ) diff --git a/tests/components/drop_connect/snapshots/test_sensor.ambr b/tests/components/drop_connect/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..54e3259e455 --- /dev/null +++ b/tests/components/drop_connect/snapshots/test_sensor.ambr @@ -0,0 +1,919 @@ +# serializer version: 1 +# name: test_sensors[filter][sensor.filter_battery-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Filter Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.filter_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_sensors[filter][sensor.filter_battery-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Filter Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.filter_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[filter][sensor.filter_current_water_pressure-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Filter Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.filter_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '263.3797174', + }) +# --- +# name: test_sensors[filter][sensor.filter_current_water_pressure-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Filter Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.filter_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[filter][sensor.filter_water_flow_rate-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Filter Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.filter_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.84', + }) +# --- +# name: test_sensors[filter][sensor.filter_water_flow_rate-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Filter Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.filter_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_average_daily_water_usage-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Hub DROP-1_C0FFEE Average daily water usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_average_daily_water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '287.691295584', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_average_daily_water_usage-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Hub DROP-1_C0FFEE Average daily water usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_average_daily_water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_battery-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Hub DROP-1_C0FFEE Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_battery-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Hub DROP-1_C0FFEE Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_current_water_pressure-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '428.8538854', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_current_water_pressure-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_high_water_pressure_today-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE High water pressure today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_high_water_pressure_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '427.474934', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_high_water_pressure_today-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE High water pressure today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_high_water_pressure_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_low_water_pressure_today-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE Low water pressure today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_low_water_pressure_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '420.580177', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_low_water_pressure_today-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Hub DROP-1_C0FFEE Low water pressure today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_low_water_pressure_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Hub DROP-1_C0FFEE Peak water flow rate today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.8', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Hub DROP-1_C0FFEE Peak water flow rate today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_total_water_used_today-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Hub DROP-1_C0FFEE Total water used today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_total_water_used_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '881.13030096168', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_total_water_used_today-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Hub DROP-1_C0FFEE Total water used today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_total_water_used_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_water_flow_rate-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Hub DROP-1_C0FFEE Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.77', + }) +# --- +# name: test_sensors[hub][sensor.hub_drop_1_c0ffee_water_flow_rate-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Hub DROP-1_C0FFEE Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hub_drop_1_c0ffee_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[leak][sensor.leak_detector_battery-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Leak Detector Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.leak_detector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[leak][sensor.leak_detector_battery-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Leak Detector Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.leak_detector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[leak][sensor.leak_detector_temperature-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Leak Detector Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.leak_detector_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.1111111111111', + }) +# --- +# name: test_sensors[leak][sensor.leak_detector_temperature-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Leak Detector Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.leak_detector_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17.7777777777778', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_battery-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Protection Valve Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.protection_valve_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_battery-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Protection Valve Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.protection_valve_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_current_water_pressure-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Protection Valve Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '422.6486041', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_current_water_pressure-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Protection Valve Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_temperature-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Protection Valve Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.3888888888889', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_temperature-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Protection Valve Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17.7777777777778', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_water_flow_rate-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Protection Valve Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.1', + }) +# --- +# name: test_sensors[protection_valve][sensor.protection_valve_water_flow_rate-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Protection Valve Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.protection_valve_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_current_water_pressure-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Pump Controller Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '428.8538854', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_current_water_pressure-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Pump Controller Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_temperature-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pump Controller Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.4444444444444', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_temperature-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pump Controller Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17.7777777777778', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_water_flow_rate-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Pump Controller Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.2', + }) +# --- +# name: test_sensors[pump_controller][sensor.pump_controller_water_flow_rate-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Pump Controller Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pump_controller_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_1_life_remaining-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 1 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_1_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '59', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_1_life_remaining-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 1 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_1_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_2_life_remaining-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 2 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_2_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_2_life_remaining-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 2 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_2_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_3_life_remaining-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 3 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_3_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '59', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_cartridge_3_life_remaining-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Cartridge 3 life remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_cartridge_3_life_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_inlet_tds-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Inlet TDS', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_inlet_tds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '164', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_inlet_tds-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Inlet TDS', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_inlet_tds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_outlet_tds-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Outlet TDS', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_outlet_tds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9', + }) +# --- +# name: test_sensors[ro_filter][sensor.ro_filter_outlet_tds-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RO Filter Outlet TDS', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.ro_filter_outlet_tds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[softener][sensor.softener_battery-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Softener Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.softener_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensors[softener][sensor.softener_battery-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Softener Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.softener_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[softener][sensor.softener_capacity_remaining-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Softener Capacity remaining', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_capacity_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3785.411784', + }) +# --- +# name: test_sensors[softener][sensor.softener_capacity_remaining-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Softener Capacity remaining', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_capacity_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[softener][sensor.softener_current_water_pressure-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Softener Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '348.1852285', + }) +# --- +# name: test_sensors[softener][sensor.softener_current_water_pressure-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Softener Current water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_current_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[softener][sensor.softener_water_flow_rate-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Softener Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[softener][sensor.softener_water_flow_rate-reset] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Softener Water flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.softener_water_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- diff --git a/tests/components/drop_connect/test_sensor.py b/tests/components/drop_connect/test_sensor.py index 43da49af884..4873d1edbd1 100644 --- a/tests/components/drop_connect/test_sensor.py +++ b/tests/components/drop_connect/test_sensor.py @@ -1,7 +1,14 @@ """Test DROP sensor entities.""" -from homeassistant.const import STATE_UNKNOWN +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .common import ( TEST_DATA_FILTER, @@ -32,288 +39,92 @@ from .common import ( config_entry_pump_controller, config_entry_ro_filter, config_entry_softener, + help_assert_entries, ) -from tests.common import async_fire_mqtt_message +from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import MqttMockHAClient -async def test_sensors_hub(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: - """Test DROP sensors for hubs.""" - entry = config_entry_hub() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - current_flow_sensor_name = "sensor.hub_drop_1_c0ffee_water_flow_rate" - assert hass.states.get(current_flow_sensor_name).state == STATE_UNKNOWN - peak_flow_sensor_name = "sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today" - assert hass.states.get(peak_flow_sensor_name).state == STATE_UNKNOWN - used_today_sensor_name = "sensor.hub_drop_1_c0ffee_total_water_used_today" - assert hass.states.get(used_today_sensor_name).state == STATE_UNKNOWN - average_usage_sensor_name = "sensor.hub_drop_1_c0ffee_average_daily_water_usage" - assert hass.states.get(average_usage_sensor_name).state == STATE_UNKNOWN - psi_sensor_name = "sensor.hub_drop_1_c0ffee_current_water_pressure" - assert hass.states.get(psi_sensor_name).state == STATE_UNKNOWN - psi_high_sensor_name = "sensor.hub_drop_1_c0ffee_high_water_pressure_today" - assert hass.states.get(psi_high_sensor_name).state == STATE_UNKNOWN - psi_low_sensor_name = "sensor.hub_drop_1_c0ffee_low_water_pressure_today" - assert hass.states.get(psi_low_sensor_name).state == STATE_UNKNOWN - battery_sensor_name = "sensor.hub_drop_1_c0ffee_battery" - assert hass.states.get(battery_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB_RESET) - await hass.async_block_till_done() - async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB) - await hass.async_block_till_done() - - current_flow_sensor = hass.states.get(current_flow_sensor_name) - assert current_flow_sensor - assert current_flow_sensor.state == "5.77" - - peak_flow_sensor = hass.states.get(peak_flow_sensor_name) - assert peak_flow_sensor - assert peak_flow_sensor.state == "13.8" - - used_today_sensor = hass.states.get(used_today_sensor_name) - assert used_today_sensor - assert used_today_sensor.state == "881.13030096168" # liters - - average_usage_sensor = hass.states.get(average_usage_sensor_name) - assert average_usage_sensor - assert average_usage_sensor.state == "287.691295584" # liters - - psi_sensor = hass.states.get(psi_sensor_name) - assert psi_sensor - assert psi_sensor.state == "428.8538854" # centibars - - psi_high_sensor = hass.states.get(psi_high_sensor_name) - assert psi_high_sensor - assert psi_high_sensor.state == "427.474934" # centibars - - psi_low_sensor = hass.states.get(psi_low_sensor_name) - assert psi_low_sensor - assert psi_low_sensor.state == "420.580177" # centibars - - battery_sensor = hass.states.get(battery_sensor_name) - assert battery_sensor - assert battery_sensor.state == "50" +@pytest.fixture(autouse=True) +def only_sensor_platform() -> Generator[[], None]: + """Only setup the DROP sensor platform.""" + with patch("homeassistant.components.drop_connect.PLATFORMS", [Platform.SENSOR]): + yield -async def test_sensors_leak(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: - """Test DROP sensors for leak detectors.""" - entry = config_entry_leak() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - battery_sensor_name = "sensor.leak_detector_battery" - assert hass.states.get(battery_sensor_name).state == STATE_UNKNOWN - temp_sensor_name = "sensor.leak_detector_temperature" - assert hass.states.get(temp_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK_RESET) - await hass.async_block_till_done() - async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK) - await hass.async_block_till_done() - - battery_sensor = hass.states.get(battery_sensor_name) - assert battery_sensor - assert battery_sensor.state == "100" - - temp_sensor = hass.states.get(temp_sensor_name) - assert temp_sensor - assert temp_sensor.state == "20.1111111111111" # °C - - -async def test_sensors_softener( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient +@pytest.mark.parametrize( + ("config_entry", "topic", "reset", "data"), + [ + (config_entry_hub(), TEST_DATA_HUB_TOPIC, TEST_DATA_HUB_RESET, TEST_DATA_HUB), + ( + config_entry_leak(), + TEST_DATA_LEAK_TOPIC, + TEST_DATA_LEAK_RESET, + TEST_DATA_LEAK, + ), + ( + config_entry_softener(), + TEST_DATA_SOFTENER_TOPIC, + TEST_DATA_SOFTENER_RESET, + TEST_DATA_SOFTENER, + ), + ( + config_entry_filter(), + TEST_DATA_FILTER_TOPIC, + TEST_DATA_FILTER_RESET, + TEST_DATA_FILTER, + ), + ( + config_entry_protection_valve(), + TEST_DATA_PROTECTION_VALVE_TOPIC, + TEST_DATA_PROTECTION_VALVE_RESET, + TEST_DATA_PROTECTION_VALVE, + ), + ( + config_entry_pump_controller(), + TEST_DATA_PUMP_CONTROLLER_TOPIC, + TEST_DATA_PUMP_CONTROLLER_RESET, + TEST_DATA_PUMP_CONTROLLER, + ), + ( + config_entry_ro_filter(), + TEST_DATA_RO_FILTER_TOPIC, + TEST_DATA_RO_FILTER_RESET, + TEST_DATA_RO_FILTER, + ), + ], + ids=[ + "hub", + "leak", + "softener", + "filter", + "protection_valve", + "pump_controller", + "ro_filter", + ], +) +async def test_sensors( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + topic: str, + reset: str, + data: str, ) -> None: - """Test DROP sensors for softeners.""" - entry = config_entry_softener() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + """Test DROP sensors.""" + config_entry.add_to_hass(hass) - battery_sensor_name = "sensor.softener_battery" - assert hass.states.get(battery_sensor_name).state == STATE_UNKNOWN - current_flow_sensor_name = "sensor.softener_water_flow_rate" - assert hass.states.get(current_flow_sensor_name).state == STATE_UNKNOWN - psi_sensor_name = "sensor.softener_current_water_pressure" - assert hass.states.get(psi_sensor_name).state == STATE_UNKNOWN - capacity_sensor_name = "sensor.softener_capacity_remaining" - assert hass.states.get(capacity_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER_RESET) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER) + help_assert_entries(hass, entity_registry, snapshot, config_entry, "init", True) + + async_fire_mqtt_message(hass, topic, reset) await hass.async_block_till_done() + help_assert_entries(hass, entity_registry, snapshot, config_entry, "reset") - battery_sensor = hass.states.get(battery_sensor_name) - assert battery_sensor - assert battery_sensor.state == "20" - - current_flow_sensor = hass.states.get(current_flow_sensor_name) - assert current_flow_sensor - assert current_flow_sensor.state == "5.0" - - psi_sensor = hass.states.get(psi_sensor_name) - assert psi_sensor - assert psi_sensor.state == "348.1852285" # centibars - - capacity_sensor = hass.states.get(capacity_sensor_name) - assert capacity_sensor - assert capacity_sensor.state == "3785.411784" # liters - - -async def test_sensors_filter(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: - """Test DROP sensors for filters.""" - entry = config_entry_filter() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - battery_sensor_name = "sensor.filter_battery" - assert hass.states.get(battery_sensor_name).state == STATE_UNKNOWN - current_flow_sensor_name = "sensor.filter_water_flow_rate" - assert hass.states.get(current_flow_sensor_name).state == STATE_UNKNOWN - psi_sensor_name = "sensor.filter_current_water_pressure" - assert hass.states.get(psi_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, TEST_DATA_FILTER_TOPIC, TEST_DATA_FILTER_RESET) + async_fire_mqtt_message(hass, topic, data) await hass.async_block_till_done() - async_fire_mqtt_message(hass, TEST_DATA_FILTER_TOPIC, TEST_DATA_FILTER) - await hass.async_block_till_done() - - battery_sensor = hass.states.get(battery_sensor_name) - assert battery_sensor - assert battery_sensor.state == "12" - - current_flow_sensor = hass.states.get(current_flow_sensor_name) - assert current_flow_sensor - assert current_flow_sensor.state == "19.84" - - psi_sensor = hass.states.get(psi_sensor_name) - assert psi_sensor - assert psi_sensor.state == "263.3797174" # centibars - - -async def test_sensors_protection_valve( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient -) -> None: - """Test DROP sensors for protection valves.""" - entry = config_entry_protection_valve() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - battery_sensor_name = "sensor.protection_valve_battery" - assert hass.states.get(battery_sensor_name).state == STATE_UNKNOWN - current_flow_sensor_name = "sensor.protection_valve_water_flow_rate" - assert hass.states.get(current_flow_sensor_name).state == STATE_UNKNOWN - psi_sensor_name = "sensor.protection_valve_current_water_pressure" - assert hass.states.get(psi_sensor_name).state == STATE_UNKNOWN - temp_sensor_name = "sensor.protection_valve_temperature" - assert hass.states.get(temp_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message( - hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE_RESET - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE - ) - await hass.async_block_till_done() - - battery_sensor = hass.states.get(battery_sensor_name) - assert battery_sensor - assert battery_sensor.state == "0" - - current_flow_sensor = hass.states.get(current_flow_sensor_name) - assert current_flow_sensor - assert current_flow_sensor.state == "7.1" - - psi_sensor = hass.states.get(psi_sensor_name) - assert psi_sensor - assert psi_sensor.state == "422.6486041" # centibars - - temp_sensor = hass.states.get(temp_sensor_name) - assert temp_sensor - assert temp_sensor.state == "21.3888888888889" # °C - - -async def test_sensors_pump_controller( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient -) -> None: - """Test DROP sensors for pump controllers.""" - entry = config_entry_pump_controller() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - current_flow_sensor_name = "sensor.pump_controller_water_flow_rate" - assert hass.states.get(current_flow_sensor_name).state == STATE_UNKNOWN - psi_sensor_name = "sensor.pump_controller_current_water_pressure" - assert hass.states.get(psi_sensor_name).state == STATE_UNKNOWN - temp_sensor_name = "sensor.pump_controller_temperature" - assert hass.states.get(temp_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message( - hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER_RESET - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER - ) - await hass.async_block_till_done() - - current_flow_sensor = hass.states.get(current_flow_sensor_name) - assert current_flow_sensor - assert current_flow_sensor.state == "2.2" - - psi_sensor = hass.states.get(psi_sensor_name) - assert psi_sensor - assert psi_sensor.state == "428.8538854" # centibars - - temp_sensor = hass.states.get(temp_sensor_name) - assert temp_sensor - assert temp_sensor.state == "20.4444444444444" # °C - - -async def test_sensors_ro_filter( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient -) -> None: - """Test DROP sensors for RO filters.""" - entry = config_entry_ro_filter() - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - tds_in_sensor_name = "sensor.ro_filter_inlet_tds" - assert hass.states.get(tds_in_sensor_name).state == STATE_UNKNOWN - tds_out_sensor_name = "sensor.ro_filter_outlet_tds" - assert hass.states.get(tds_out_sensor_name).state == STATE_UNKNOWN - cart1_sensor_name = "sensor.ro_filter_cartridge_1_life_remaining" - assert hass.states.get(cart1_sensor_name).state == STATE_UNKNOWN - cart2_sensor_name = "sensor.ro_filter_cartridge_2_life_remaining" - assert hass.states.get(cart2_sensor_name).state == STATE_UNKNOWN - cart3_sensor_name = "sensor.ro_filter_cartridge_3_life_remaining" - assert hass.states.get(cart3_sensor_name).state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER_RESET) - await hass.async_block_till_done() - async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER) - await hass.async_block_till_done() - - tds_in_sensor = hass.states.get(tds_in_sensor_name) - assert tds_in_sensor - assert tds_in_sensor.state == "164" - - tds_out_sensor = hass.states.get(tds_out_sensor_name) - assert tds_out_sensor - assert tds_out_sensor.state == "9" - - cart1_sensor = hass.states.get(cart1_sensor_name) - assert cart1_sensor - assert cart1_sensor.state == "59" - - cart2_sensor = hass.states.get(cart2_sensor_name) - assert cart2_sensor - assert cart2_sensor.state == "80" - - cart3_sensor = hass.states.get(cart3_sensor_name) - assert cart3_sensor - assert cart3_sensor.state == "59" + help_assert_entries(hass, entity_registry, snapshot, config_entry, "data") diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index 360efc390db..e5b82d0c453 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -6,6 +6,9 @@ from homeassistant.components.dwd_weather_warnings.const import ( CONF_REGION_DEVICE_TRACKER, DOMAIN, ) +from homeassistant.components.dwd_weather_warnings.coordinator import ( + DwdWeatherWarningsCoordinator, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.core import HomeAssistant @@ -25,13 +28,12 @@ async def test_load_unload_entry( entry = await init_integration(hass, mock_identifier_entry) assert entry.state is ConfigEntryState.LOADED - assert entry.entry_id in hass.data[DOMAIN] + assert isinstance(entry.runtime_data, DwdWeatherWarningsCoordinator) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED - assert entry.entry_id not in hass.data[DOMAIN] async def test_load_invalid_registry_entry( @@ -97,4 +99,4 @@ async def test_load_valid_device_tracker( await hass.async_block_till_done() assert mock_tracker_entry.state is ConfigEntryState.LOADED - assert mock_tracker_entry.entry_id in hass.data[DOMAIN] + assert isinstance(mock_tracker_entry.runtime_data, DwdWeatherWarningsCoordinator) diff --git a/tests/components/eq3btsmart/conftest.py b/tests/components/eq3btsmart/conftest.py index 19e10d6b59c..b16c5088044 100644 --- a/tests/components/eq3btsmart/conftest.py +++ b/tests/components/eq3btsmart/conftest.py @@ -38,4 +38,5 @@ def fake_service_info(): tx_power=-127, platform_data=(), ), + tx_power=-127, ) diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index a55d7ea84c0..7530068dc88 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -16,4 +16,5 @@ COOKER_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index 9250c26926a..63af6340ade 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -1,6 +1,7 @@ """Test helpers.""" from collections.abc import Generator +from datetime import UTC, datetime, timedelta from unittest.mock import AsyncMock, patch import pytest @@ -32,14 +33,17 @@ def mock_fyta_init(): """Build a fixture for the Fyta API that connects successfully and returns one device.""" mock_fyta_api = AsyncMock() - with patch( - "homeassistant.components.fyta.FytaConnector", - return_value=mock_fyta_api, - ) as mock_fyta_api: - mock_fyta_api.return_value.login.return_value = { + mock_fyta_api.expiration = datetime.now(tz=UTC) + timedelta(days=1) + mock_fyta_api.login = AsyncMock( + return_value={ CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_EXPIRATION: EXPIRATION, } + ) + with patch( + "homeassistant.components.fyta.FytaConnector.__new__", + return_value=mock_fyta_api, + ): yield mock_fyta_api diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index 69478d04ca0..dedb468a617 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry USERNAME = "fyta_user" PASSWORD = "fyta_pass" ACCESS_TOKEN = "123xyz" -EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC) +EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").replace(tzinfo=UTC) async def test_user_flow( diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 9168e29f2d5..50c7e664cd4 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -55,7 +55,7 @@ def common_requests(aioclient_mock): "api_user": "test-api-user", "profile": {"name": TEST_USER_NAME}, "stats": { - "class": "test-class", + "class": "warrior", "con": 1, "exp": 2, "gp": 3, @@ -78,7 +78,11 @@ def common_requests(aioclient_mock): f"https://habitica.com/api/v3/tasks/user?type={task_type}", json={ "data": [ - {"text": f"this is a mock {task_type} #{task}", "id": f"{task}"} + { + "text": f"this is a mock {task_type} #{task}", + "id": f"{task}", + "type": TASKS_TYPES[task_type].path[0], + } for task in range(n_tasks) ] }, diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index d09444808d8..b57be5f1838 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -29,13 +29,19 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.components.honeywell.climate import PRESET_HOLD, RETRY, SCAN_INTERVAL +from homeassistant.components.honeywell.climate import ( + DOMAIN, + PRESET_HOLD, + RETRY, + SCAN_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -1264,3 +1270,22 @@ async def test_aux_heat_off_service_call( blocking=True, ) device.set_system_mode.assert_called_once_with("off") + + +async def test_unique_id( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test unique id convert to string.""" + entity_registry.async_get_or_create( + Platform.CLIMATE, + DOMAIN, + device.deviceid, + config_entry=config_entry, + suggested_object_id=device.name, + ) + await init_integration(hass, config_entry) + entity_entry = entity_registry.async_get(f"climate.{device.name}") + assert entity_entry.unique_id == str(device.deviceid) diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index 77f8271370e..481a1315325 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -235,6 +235,7 @@ async def test_device_tracker_random_address_infrequent_changes( connectable=False, device=device, advertisement=previous_service_info.advertisement, + tx_power=-127, ), ) device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False) diff --git a/tests/components/idasen_desk/__init__.py b/tests/components/idasen_desk/__init__.py index 7e8becc4689..b0d7cc5ac05 100644 --- a/tests/components/idasen_desk/__init__.py +++ b/tests/components/idasen_desk/__init__.py @@ -20,6 +20,7 @@ IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak( @@ -34,6 +35,7 @@ NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/improv_ble/__init__.py b/tests/components/improv_ble/__init__.py index f1c83bbc0d7..41ea98cda7b 100644 --- a/tests/components/improv_ble/__init__.py +++ b/tests/components/improv_ble/__init__.py @@ -21,6 +21,7 @@ IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( ), time=0, connectable=True, + tx_power=-127, ) @@ -39,6 +40,7 @@ PROVISIONED_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( ), time=0, connectable=True, + tx_power=-127, ) @@ -57,4 +59,5 @@ NOT_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 242c1ebe7d6..1e717b805c5 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -46,6 +46,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "mibp"), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/ld2410_ble/__init__.py b/tests/components/ld2410_ble/__init__.py index b38115aab4d..f4e6dfc2501 100644 --- a/tests/components/ld2410_ble/__init__.py +++ b/tests/components/ld2410_ble/__init__.py @@ -16,6 +16,7 @@ LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( @@ -33,4 +34,5 @@ NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/led_ble/__init__.py b/tests/components/led_ble/__init__.py index 10eaf758757..2810ba475d2 100644 --- a/tests/components/led_ble/__init__.py +++ b/tests/components/led_ble/__init__.py @@ -18,6 +18,7 @@ LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) UNSUPPORTED_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( @@ -34,6 +35,7 @@ UNSUPPORTED_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) @@ -52,4 +54,5 @@ NOT_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/medcom_ble/__init__.py b/tests/components/medcom_ble/__init__.py index aa367b93a14..5afaa01f85e 100644 --- a/tests/components/medcom_ble/__init__.py +++ b/tests/components/medcom_ble/__init__.py @@ -75,6 +75,7 @@ MEDCOM_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=-127, ) UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -95,6 +96,7 @@ UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=-127, ) MEDCOM_DEVICE_INFO = MedcomBleDevice( diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 361102f22e6..b75eb370555 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -35,6 +35,7 @@ FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name=""), time=0, connectable=True, + tx_power=-127, ) FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( @@ -51,6 +52,7 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name=""), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 887d20d71ce..f5a988a628d 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -39,6 +39,7 @@ BLIND_SERVICE_INFO = BluetoothServiceInfoBleak( ), connectable=True, time=0, + tx_power=-127, ) diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 1ecfc0d9ecf..cfafae28b6e 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.notify import ( SERVICE_SEND_MESSAGE, NotifyEntity, NotifyEntityDescription, + NotifyEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform @@ -27,7 +28,8 @@ from tests.common import ( setup_test_component_platform, ) -TEST_KWARGS = {"message": "Test message"} +TEST_KWARGS = {notify.ATTR_MESSAGE: "Test message"} +TEST_KWARGS_TITLE = {notify.ATTR_MESSAGE: "Test message", notify.ATTR_TITLE: "My title"} class MockNotifyEntity(MockEntity, NotifyEntity): @@ -35,9 +37,9 @@ class MockNotifyEntity(MockEntity, NotifyEntity): send_message_mock_calls = MagicMock() - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a notification message.""" - self.send_message_mock_calls(message=message) + self.send_message_mock_calls(message, title=title) class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): @@ -45,9 +47,9 @@ class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): send_message_mock_calls = MagicMock() - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a notification message.""" - self.send_message_mock_calls(message=message) + self.send_message_mock_calls(message, title=title) async def help_async_setup_entry_init( @@ -132,6 +134,58 @@ async def test_send_message_service( assert await hass.config_entries.async_unload(config_entry.entry_id) +@pytest.mark.parametrize( + "entity", + [ + MockNotifyEntityNonAsync( + name="test", + entity_id="notify.test", + supported_features=NotifyEntityFeature.TITLE, + ), + MockNotifyEntity( + name="test", + entity_id="notify.test", + supported_features=NotifyEntityFeature.TITLE, + ), + ], + ids=["non_async", "async"], +) +async def test_send_message_service_with_title( + hass: HomeAssistant, config_flow_fixture: None, entity: NotifyEntity +) -> None: + """Test send_message service.""" + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get("notify.test") + assert state.state is STATE_UNKNOWN + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + copy.deepcopy(TEST_KWARGS_TITLE) | {"entity_id": "notify.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + entity.send_message_mock_calls.assert_called_once_with( + TEST_KWARGS_TITLE[notify.ATTR_MESSAGE], + title=TEST_KWARGS_TITLE[notify.ATTR_TITLE], + ) + + @pytest.mark.parametrize( ("state", "init_state"), [ @@ -202,12 +256,12 @@ async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None: state = hass.states.get(entity1.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)} state = hass.states.get(entity2.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)} state = hass.states.get(entity3.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)} diff --git a/tests/components/nut/test_device_action.py b/tests/components/nut/test_device_action.py index 8113b19e313..01675f928e3 100644 --- a/tests/components/nut/test_device_action.py +++ b/tests/components/nut/test_device_action.py @@ -7,9 +7,13 @@ import pytest from pytest_unordered import unordered from homeassistant.components import automation, device_automation -from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.device_automation import ( + DeviceAutomationType, + InvalidDeviceAutomationConfig, +) from homeassistant.components.nut import DOMAIN from homeassistant.components.nut.const import INTEGRATION_SUPPORTED_COMMANDS +from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -229,3 +233,25 @@ async def test_rund_command_exception( await hass.async_block_till_done() assert error_message in caplog.text + + +async def test_action_exception_invalid_device(hass: HomeAssistant) -> None: + """Test raises exception if invalid device.""" + list_commands_return_value = {"beeper.enable": None} + await async_init_integration( + hass, + list_vars={"ups.status": "OL"}, + list_commands_return_value=list_commands_return_value, + ) + + platform = await device_automation.async_get_device_automation_platform( + hass, DOMAIN, DeviceAutomationType.ACTION + ) + + with pytest.raises(InvalidDeviceAutomationConfig): + await platform.async_call_action_from_config( + hass, + {CONF_TYPE: "beeper.enable", CONF_DEVICE_ID: "invalid_device_id"}, + {}, + None, + ) diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index ac2c281c57b..48401fe87ba 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -11,6 +11,7 @@ from .const import DEFAULT_FORECAST, DEFAULT_OBSERVATION @pytest.fixture def mock_simple_nws(): """Mock pynws SimpleNWS with default values.""" + with patch("homeassistant.components.nws.SimpleNWS") as mock_nws: instance = mock_nws.return_value instance.set_station = AsyncMock(return_value=None) diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index ad40b576a8a..87aae18be60 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -13,7 +13,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN @@ -181,7 +180,7 @@ async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) - await hass.async_block_till_done() assert instance.update_observation.call_count == 2 assert instance.update_forecast.call_count == 2 - instance.update_forecast_hourly.assert_called_once() + assert instance.update_forecast_hourly.call_count == 2 async def test_error_observation( @@ -189,18 +188,8 @@ async def test_error_observation( ) -> None: """Test error during update observation.""" utc_time = dt_util.utcnow() - with ( - patch("homeassistant.components.nws.utcnow") as mock_utc, - patch("homeassistant.components.nws.weather.utcnow") as mock_utc_weather, - ): - - def increment_time(time): - mock_utc.return_value += time - mock_utc_weather.return_value += time - async_fire_time_changed(hass, mock_utc.return_value) - + with patch("homeassistant.components.nws.utcnow") as mock_utc: mock_utc.return_value = utc_time - mock_utc_weather.return_value = utc_time instance = mock_simple_nws.return_value # first update fails instance.update_observation.side_effect = aiohttp.ClientError @@ -219,68 +208,6 @@ async def test_error_observation( assert state assert state.state == STATE_UNAVAILABLE - # second update happens faster and succeeds - instance.update_observation.side_effect = None - increment_time(timedelta(minutes=1)) - await hass.async_block_till_done() - - assert instance.update_observation.call_count == 2 - - state = hass.states.get("weather.abc") - assert state - assert state.state == ATTR_CONDITION_SUNNY - - # third udate fails, but data is cached - instance.update_observation.side_effect = aiohttp.ClientError - - increment_time(timedelta(minutes=10)) - await hass.async_block_till_done() - - assert instance.update_observation.call_count == 3 - - state = hass.states.get("weather.abc") - assert state - assert state.state == ATTR_CONDITION_SUNNY - - # after 20 minutes data caching expires, data is no longer shown - increment_time(timedelta(minutes=10)) - await hass.async_block_till_done() - - state = hass.states.get("weather.abc") - assert state - assert state.state == STATE_UNAVAILABLE - - -async def test_error_forecast(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: - """Test error during update forecast.""" - instance = mock_simple_nws.return_value - instance.update_forecast.side_effect = aiohttp.ClientError - - entry = MockConfigEntry( - domain=nws.DOMAIN, - data=NWS_CONFIG, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - instance.update_forecast.assert_called_once() - - state = hass.states.get("weather.abc") - assert state - assert state.state == STATE_UNAVAILABLE - - instance.update_forecast.side_effect = None - - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() - - assert instance.update_forecast.call_count == 2 - - state = hass.states.get("weather.abc") - assert state - assert state.state == ATTR_CONDITION_SUNNY - async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None: """Test the expected entities are created.""" @@ -304,7 +231,6 @@ async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None: ("service"), [ SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, ], ) async def test_forecast_service( @@ -355,7 +281,7 @@ async def test_forecast_service( assert instance.update_observation.call_count == 2 assert instance.update_forecast.call_count == 2 - assert instance.update_forecast_hourly.call_count == 1 + assert instance.update_forecast_hourly.call_count == 2 for forecast_type in ("twice_daily", "hourly"): response = await hass.services.async_call( diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py index 668f8804a5e..757a10d22a1 100644 --- a/tests/components/oralb/__init__.py +++ b/tests/components/oralb/__init__.py @@ -49,4 +49,5 @@ ORALB_IO_SERIES_6_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index a58a46680bb..3c8f66a82d0 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -7,11 +7,13 @@ from hole.exceptions import HoleError import pytest from homeassistant.components import pi_hole, switch +from homeassistant.components.pi_hole import PiHoleData from homeassistant.components.pi_hole.const import ( CONF_STATISTICS_ONLY, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant @@ -182,12 +184,13 @@ async def test_unload(hass: HomeAssistant) -> None: with _patch_init_hole(mocked_hole): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.entry_id in hass.data[pi_hole.DOMAIN] + assert entry.state is ConfigEntryState.LOADED + assert isinstance(entry.runtime_data, PiHoleData) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.entry_id not in hass.data[pi_hole.DOMAIN] + assert entry.state is ConfigEntryState.NOT_LOADED async def test_remove_obsolete(hass: HomeAssistant) -> None: diff --git a/tests/components/private_ble_device/__init__.py b/tests/components/private_ble_device/__init__.py index b85f29fc394..8e31dbdec7a 100644 --- a/tests/components/private_ble_device/__init__.py +++ b/tests/components/private_ble_device/__init__.py @@ -63,6 +63,7 @@ async def async_inject_broadcast( advertisement=generate_advertisement_data(local_name="Not it"), time=broadcast_time or time.monotonic(), connectable=False, + tx_power=-127, ), ) await hass.async_block_till_done() diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index e0f43323f25..2ded3513a7e 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -257,6 +257,11 @@ def assert_dict_of_states_equal_without_context_and_last_changed( ) +async def async_record_states(hass: HomeAssistant): + """Record some test states.""" + return await hass.async_add_executor_job(record_states, hass) + + def record_states(hass): """Record some test states. diff --git a/tests/components/recorder/test_entity_registry.py b/tests/components/recorder/test_entity_registry.py index 37223f206a1..a74992525b1 100644 --- a/tests/components/recorder/test_entity_registry.py +++ b/tests/components/recorder/test_entity_registry.py @@ -1,6 +1,5 @@ """The tests for sensor recorder platform.""" -from collections.abc import Callable from unittest.mock import patch import pytest @@ -8,23 +7,22 @@ from sqlalchemy import select from sqlalchemy.orm import Session from homeassistant.components import recorder -from homeassistant.components.recorder import history +from homeassistant.components.recorder import Recorder, history from homeassistant.components.recorder.db_schema import StatesMeta from homeassistant.components.recorder.util import session_scope -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .common import ( ForceReturnConnectionToPool, assert_dict_of_states_equal_without_context_and_last_changed, + async_record_states, async_wait_recording_done, - record_states, - wait_recording_done, ) -from tests.common import MockEntity, MockEntityPlatform, mock_registry +from tests.common import MockEntity, MockEntityPlatform from tests.typing import RecorderInstanceGenerator @@ -40,41 +38,44 @@ def _count_entity_id_in_states_meta( ) -def test_rename_entity_without_collision( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +@pytest.fixture +async def mock_recorder_before_hass( + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + +@pytest.fixture(autouse=True) +def setup_recorder(recorder_mock: Recorder) -> recorder.Recorder: + """Set up recorder.""" + + +async def test_rename_entity_without_collision( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test states meta is migrated when entity_id is changed.""" - hass = hass_recorder() - setup_component(hass, "sensor", {}) + await async_setup_component(hass, "sensor", {}) - entity_reg = mock_registry(hass) + reg_entry = entity_registry.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + await hass.async_block_till_done() - @callback - def add_entry(): - reg_entry = entity_reg.async_get_or_create( - "sensor", - "test", - "unique_0000", - suggested_object_id="test1", - ) - assert reg_entry.entity_id == "sensor.test1" - - hass.add_job(add_entry) - hass.block_till_done() - - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = history.get_significant_states( hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - @callback - def rename_entry(): - entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") - - hass.add_job(rename_entry) - wait_recording_done(hass) + entity_registry.async_update_entity("sensor.test1", new_entity_id="sensor.test99") + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) @@ -82,8 +83,8 @@ def test_rename_entity_without_collision( states["sensor.test99"] = states.pop("sensor.test1") assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - hass.states.set("sensor.test99", "post_migrate") - wait_recording_done(hass) + hass.states.async_set("sensor.test99", "post_migrate") + await async_wait_recording_done(hass) new_hist = history.get_significant_states( hass, zero, @@ -101,8 +102,8 @@ def test_rename_entity_without_collision( async def test_rename_entity_on_mocked_platform( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test states meta is migrated when entity_id is changed when using a mocked platform. @@ -111,11 +112,10 @@ async def test_rename_entity_on_mocked_platform( sure that we do not record the entity as removed in the database when we rename it. """ - instance = await async_setup_recorder_instance(hass) - entity_reg = er.async_get(hass) + instance = recorder.get_instance(hass) start = dt_util.utcnow() - reg_entry = entity_reg.async_get_or_create( + reg_entry = entity_registry.async_get_or_create( "sensor", "test", "unique_0000", @@ -142,7 +142,7 @@ async def test_rename_entity_on_mocked_platform( ["sensor.test1", "sensor.test99"], ) - entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") + entity_registry.async_update_entity("sensor.test1", new_entity_id="sensor.test99") await hass.async_block_till_done() # We have to call the remove method ourselves since we are mocking the platform hass.states.async_remove("sensor.test1") @@ -196,47 +196,38 @@ async def test_rename_entity_on_mocked_platform( assert "the new entity_id is already in use" not in caplog.text -def test_rename_entity_collision( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_rename_entity_collision( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test states meta is not migrated when there is a collision.""" - hass = hass_recorder() - setup_component(hass, "sensor", {}) + await async_setup_component(hass, "sensor", {}) - entity_reg = mock_registry(hass) + reg_entry = entity_registry.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + await hass.async_block_till_done() - @callback - def add_entry(): - reg_entry = entity_reg.async_get_or_create( - "sensor", - "test", - "unique_0000", - suggested_object_id="test1", - ) - assert reg_entry.entity_id == "sensor.test1" - - hass.add_job(add_entry) - hass.block_till_done() - - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = history.get_significant_states( hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) assert len(hist["sensor.test1"]) == 3 - hass.states.set("sensor.test99", "collision") - hass.states.remove("sensor.test99") + hass.states.async_set("sensor.test99", "collision") + hass.states.async_remove("sensor.test99") - hass.block_till_done() + await hass.async_block_till_done() # Rename entity sensor.test1 to sensor.test99 - @callback - def rename_entry(): - entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") - - hass.add_job(rename_entry) - wait_recording_done(hass) + entity_registry.async_update_entity("sensor.test1", new_entity_id="sensor.test99") + await async_wait_recording_done(hass) # History is not migrated on collision hist = history.get_significant_states( @@ -248,8 +239,8 @@ def test_rename_entity_collision( with session_scope(hass=hass) as session: assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1 - hass.states.set("sensor.test99", "post_migrate") - wait_recording_done(hass) + hass.states.async_set("sensor.test99", "post_migrate") + await async_wait_recording_done(hass) new_hist = history.get_significant_states( hass, zero, @@ -270,44 +261,39 @@ def test_rename_entity_collision( assert "Blocked attempt to insert duplicated state rows" not in caplog.text -def test_rename_entity_collision_without_states_meta_safeguard( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_rename_entity_collision_without_states_meta_safeguard( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test states meta is not migrated when there is a collision. This test disables the safeguard in the states_meta_manager and relies on the filter_unique_constraint_integrity_error safeguard. """ - hass = hass_recorder() - setup_component(hass, "sensor", {}) + await async_setup_component(hass, "sensor", {}) - entity_reg = mock_registry(hass) + reg_entry = entity_registry.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + await hass.async_block_till_done() - @callback - def add_entry(): - reg_entry = entity_reg.async_get_or_create( - "sensor", - "test", - "unique_0000", - suggested_object_id="test1", - ) - assert reg_entry.entity_id == "sensor.test1" - - hass.add_job(add_entry) - hass.block_till_done() - - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = history.get_significant_states( hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) assert len(hist["sensor.test1"]) == 3 - hass.states.set("sensor.test99", "collision") - hass.states.remove("sensor.test99") + hass.states.async_set("sensor.test99", "collision") + hass.states.async_remove("sensor.test99") - hass.block_till_done() - wait_recording_done(hass) + await hass.async_block_till_done() + await async_wait_recording_done(hass) # Verify history before collision hist = history.get_significant_states( @@ -321,14 +307,10 @@ def test_rename_entity_collision_without_states_meta_safeguard( # so that we hit the filter_unique_constraint_integrity_error safeguard in the entity_registry with patch.object(instance.states_meta_manager, "get", return_value=None): # Rename entity sensor.test1 to sensor.test99 - @callback - def rename_entry(): - entity_reg.async_update_entity( - "sensor.test1", new_entity_id="sensor.test99" - ) - - hass.add_job(rename_entry) - wait_recording_done(hass) + entity_registry.async_update_entity( + "sensor.test1", new_entity_id="sensor.test99" + ) + await async_wait_recording_done(hass) # History is not migrated on collision hist = history.get_significant_states( @@ -340,8 +322,8 @@ def test_rename_entity_collision_without_states_meta_safeguard( with session_scope(hass=hass) as session: assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1 - hass.states.set("sensor.test99", "post_migrate") - wait_recording_done(hass) + hass.states.async_set("sensor.test99", "post_migrate") + await async_wait_recording_done(hass) new_hist = history.get_significant_states( hass, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a7aaf938410..ec43d81fc4a 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,9 +1,9 @@ """The tests for sensor recorder platform.""" -from collections.abc import Callable from datetime import datetime, timedelta import math from statistics import mean +from typing import Literal from unittest.mock import patch from freezegun import freeze_time @@ -12,6 +12,7 @@ import pytest from homeassistant import loader from homeassistant.components.recorder import ( + CONF_COMMIT_INTERVAL, DOMAIN as RECORDER_DOMAIN, Recorder, history, @@ -36,7 +37,7 @@ from homeassistant.components.recorder.util import get_instance, session_scope from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @@ -48,10 +49,9 @@ from tests.components.recorder.common import ( async_wait_recording_done, do_adhoc_statistics, statistics_during_period, - wait_recording_done, ) from tests.components.sensor.common import MockSensor -from tests.typing import WebSocketGenerator +from tests.typing import RecorderInstanceGenerator, WebSocketGenerator BATTERY_SENSOR_ATTRIBUTES = { "device_class": "battery", @@ -92,14 +92,27 @@ KW_SENSOR_ATTRIBUTES = { } +@pytest.fixture +async def mock_recorder_before_hass( + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Set up recorder patches.""" + + @pytest.fixture(autouse=True) -def set_time_zone(): - """Set the time zone for the tests.""" - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) - yield - dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) +def setup_recorder(recorder_mock: Recorder) -> Recorder: + """Set up recorder.""" + + +async def async_list_statistic_ids( + hass: HomeAssistant, + statistic_ids: set[str] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, +) -> list[dict]: + """Return all statistic_ids and unit of measurement.""" + return await hass.async_add_executor_job( + list_statistic_ids, hass, statistic_ids, statistic_type + ) @pytest.mark.parametrize( @@ -136,8 +149,8 @@ def set_time_zone(): ("weight", "oz", "oz", "oz", "mass", 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -150,24 +163,27 @@ def test_compile_hourly_statistics( ) -> None: """Test compiling hourly statistics.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -214,8 +230,8 @@ def test_compile_hourly_statistics( ("temperature", "°F", "°F", "°F", "temperature", 27.796610169491526, -10, 60), ], ) -def test_compile_hourly_statistics_with_some_same_last_updated( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_with_some_same_last_updated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -231,9 +247,9 @@ def test_compile_hourly_statistics_with_some_same_last_updated( If the last updated value is the same we will have a zero duration. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) entity_id = "sensor.test1" attributes = { "device_class": device_class, @@ -243,10 +259,10 @@ def test_compile_hourly_statistics_with_some_same_last_updated( attributes = dict(attributes) seq = [-10, 15, 30, 60] - def set_state(entity_id, state, **kwargs): + async def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) + await async_wait_recording_done(hass) return hass.states.get(entity_id) one = zero + timedelta(seconds=1 * 5) @@ -257,21 +273,21 @@ def test_compile_hourly_statistics_with_some_same_last_updated( states = {entity_id: []} with freeze_time(one) as freezer: states[entity_id].append( - set_state(entity_id, str(seq[0]), attributes=attributes) + await set_state(entity_id, str(seq[0]), attributes=attributes) ) # Record two states at the exact same time freezer.move_to(two) states[entity_id].append( - set_state(entity_id, str(seq[1]), attributes=attributes) + await set_state(entity_id, str(seq[1]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[2]), attributes=attributes) + await set_state(entity_id, str(seq[2]), attributes=attributes) ) freezer.move_to(three) states[entity_id].append( - set_state(entity_id, str(seq[3]), attributes=attributes) + await set_state(entity_id, str(seq[3]), attributes=attributes) ) hist = history.get_significant_states( @@ -280,8 +296,8 @@ def test_compile_hourly_statistics_with_some_same_last_updated( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -328,8 +344,8 @@ def test_compile_hourly_statistics_with_some_same_last_updated( ("temperature", "°F", "°F", "°F", "temperature", 60, -10, 60), ], ) -def test_compile_hourly_statistics_with_all_same_last_updated( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_with_all_same_last_updated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -345,9 +361,9 @@ def test_compile_hourly_statistics_with_all_same_last_updated( If the last updated value is the same we will have a zero duration. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) entity_id = "sensor.test1" attributes = { "device_class": device_class, @@ -357,10 +373,10 @@ def test_compile_hourly_statistics_with_all_same_last_updated( attributes = dict(attributes) seq = [-10, 15, 30, 60] - def set_state(entity_id, state, **kwargs): + async def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) + await async_wait_recording_done(hass) return hass.states.get(entity_id) one = zero + timedelta(seconds=1 * 5) @@ -371,16 +387,16 @@ def test_compile_hourly_statistics_with_all_same_last_updated( states = {entity_id: []} with freeze_time(two): states[entity_id].append( - set_state(entity_id, str(seq[0]), attributes=attributes) + await set_state(entity_id, str(seq[0]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[1]), attributes=attributes) + await set_state(entity_id, str(seq[1]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[2]), attributes=attributes) + await set_state(entity_id, str(seq[2]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[3]), attributes=attributes) + await set_state(entity_id, str(seq[3]), attributes=attributes) ) hist = history.get_significant_states( @@ -389,8 +405,8 @@ def test_compile_hourly_statistics_with_all_same_last_updated( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -437,8 +453,8 @@ def test_compile_hourly_statistics_with_all_same_last_updated( ("temperature", "°F", "°F", "°F", "temperature", 0, 60, 60), ], ) -def test_compile_hourly_statistics_only_state_is_and_end_of_period( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_only_state_is_and_end_of_period( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -451,9 +467,9 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period( ) -> None: """Test compiling hourly statistics when the only state at end of period.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) entity_id = "sensor.test1" attributes = { "device_class": device_class, @@ -463,10 +479,10 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period( attributes = dict(attributes) seq = [-10, 15, 30, 60] - def set_state(entity_id, state, **kwargs): + async def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) + await async_wait_recording_done(hass) return hass.states.get(entity_id) one = zero + timedelta(seconds=1 * 5) @@ -478,16 +494,16 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period( states = {entity_id: []} with freeze_time(end): states[entity_id].append( - set_state(entity_id, str(seq[0]), attributes=attributes) + await set_state(entity_id, str(seq[0]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[1]), attributes=attributes) + await set_state(entity_id, str(seq[1]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[2]), attributes=attributes) + await set_state(entity_id, str(seq[2]), attributes=attributes) ) states[entity_id].append( - set_state(entity_id, str(seq[3]), attributes=attributes) + await set_state(entity_id, str(seq[3]), attributes=attributes) ) hist = history.get_significant_states( @@ -496,8 +512,8 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -534,8 +550,8 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period( (None, "%", "%", "%", "unitless"), ], ) -def test_compile_hourly_statistics_purged_state_changes( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_purged_state_changes( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -545,16 +561,19 @@ def test_compile_hourly_statistics_purged_state_changes( ) -> None: """Test compiling hourly statistics.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) @@ -564,17 +583,17 @@ def test_compile_hourly_statistics_purged_state_changes( # Purge all states from the database with freeze_time(four): - hass.services.call("recorder", "purge", {"keep_days": 0}) - hass.block_till_done() - wait_recording_done(hass) + await hass.services.async_call("recorder", "purge", {"keep_days": 0}) + await hass.async_block_till_done() + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert not hist do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -606,42 +625,57 @@ def test_compile_hourly_statistics_purged_state_changes( @pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES]) -def test_compile_hourly_statistics_wrong_unit( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_wrong_unit( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, attributes, ) -> None: """Test compiling hourly statistics for sensor with unit not matching device class.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) attributes_tmp = dict(attributes) attributes_tmp["unit_of_measurement"] = "invalid" - _, _states = record_states(hass, freezer, zero, "sensor.test2", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test2", attributes_tmp + ) states = {**states, **_states} attributes_tmp.pop("unit_of_measurement") - _, _states = record_states(hass, freezer, zero, "sensor.test3", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test3", attributes_tmp + ) states = {**states, **_states} attributes_tmp = dict(attributes) attributes_tmp["state_class"] = "invalid" - _, _states = record_states(hass, freezer, zero, "sensor.test4", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test4", attributes_tmp + ) states = {**states, **_states} attributes_tmp.pop("state_class") - _, _states = record_states(hass, freezer, zero, "sensor.test5", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test5", attributes_tmp + ) states = {**states, **_states} attributes_tmp = dict(attributes) attributes_tmp["device_class"] = "invalid" - _, _states = record_states(hass, freezer, zero, "sensor.test6", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test6", attributes_tmp + ) states = {**states, **_states} attributes_tmp.pop("device_class") - _, _states = record_states(hass, freezer, zero, "sensor.test7", attributes_tmp) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test7", attributes_tmp + ) states = {**states, **_states} + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -649,8 +683,8 @@ def test_compile_hourly_statistics_wrong_unit( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -808,7 +842,6 @@ def test_compile_hourly_statistics_wrong_unit( ], ) async def test_compile_hourly_sum_statistics_amount( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, caplog: pytest.LogCaptureFixture, @@ -838,8 +871,8 @@ async def test_compile_hourly_sum_statistics_amount( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] with freeze_time(period0) as freezer: - four, eight, states = await hass.async_add_executor_job( - record_meter_states, hass, freezer, period0, "sensor.test1", attributes, seq + four, eight, states = await async_record_meter_states( + hass, freezer, period0, "sensor.test1", attributes, seq ) await async_wait_recording_done(hass) hist = history.get_significant_states( @@ -858,7 +891,7 @@ async def test_compile_hourly_sum_statistics_amount( await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period2) await async_wait_recording_done(hass) - statistic_ids = await hass.async_add_executor_job(list_statistic_ids, hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -994,8 +1027,8 @@ async def test_compile_hourly_sum_statistics_amount( ("weight", "kg", "kg", "kg", "mass", 1), ], ) -def test_compile_hourly_sum_statistics_amount_reset_every_state_change( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_amount_reset_every_state_change( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, state_class, device_class, @@ -1007,9 +1040,9 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( ) -> None: """Test compiling hourly statistics.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": state_class, @@ -1031,7 +1064,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( one = one + timedelta(seconds=5) attributes = dict(attributes) attributes["last_reset"] = dt_util.as_local(one).isoformat() - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, one, "sensor.test1", attributes, seq[i : i + 1] ) states["sensor.test1"].extend(_states["sensor.test1"]) @@ -1042,10 +1075,11 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( two = two + timedelta(seconds=5) attributes = dict(attributes) attributes["last_reset"] = dt_util.as_local(two).isoformat() - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, two, "sensor.test1", attributes, seq[i : i + 1] ) states["sensor.test1"].extend(_states["sensor.test1"]) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, @@ -1060,8 +1094,8 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( do_adhoc_statistics(hass, start=zero) do_adhoc_statistics(hass, start=zero + timedelta(minutes=5)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1116,8 +1150,8 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( ("energy", "kWh", "kWh", "kWh", "energy", 1), ], ) -def test_compile_hourly_sum_statistics_amount_invalid_last_reset( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_amount_invalid_last_reset( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, state_class, device_class, @@ -1129,9 +1163,9 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( ) -> None: """Test compiling hourly statistics.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": state_class, @@ -1151,10 +1185,11 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( attributes["last_reset"] = dt_util.as_local(one).isoformat() if i == 3: attributes["last_reset"] = "festivus" # not a valid time - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, one, "sensor.test1", attributes, seq[i : i + 1] ) states["sensor.test1"].extend(_states["sensor.test1"]) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, @@ -1168,8 +1203,8 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( ) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1215,8 +1250,8 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( ("energy", "kWh", "kWh", "kWh", "energy", 1), ], ) -def test_compile_hourly_sum_statistics_nan_inf_state( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_nan_inf_state( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, state_class, device_class, @@ -1228,9 +1263,9 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ) -> None: """Test compiling hourly statistics with nan and inf states.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": state_class, @@ -1246,10 +1281,11 @@ def test_compile_hourly_sum_statistics_nan_inf_state( one = one + timedelta(seconds=5) attributes = dict(attributes) attributes["last_reset"] = dt_util.as_local(one).isoformat() - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, one, "sensor.test1", attributes, seq[i : i + 1] ) states["sensor.test1"].extend(_states["sensor.test1"]) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, @@ -1263,8 +1299,8 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1346,8 +1382,8 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ], ) @pytest.mark.parametrize("state_class", ["total_increasing"]) -def test_compile_hourly_sum_statistics_negative_state( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_negative_state( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_id, warning_1, @@ -1362,18 +1398,17 @@ def test_compile_hourly_sum_statistics_negative_state( ) -> None: """Test compiling hourly statistics with negative states.""" zero = dt_util.utcnow() - hass = hass_recorder() hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) mocksensor = MockSensor(name="custom_sensor") mocksensor._attr_should_poll = False setup_test_component_platform(hass, DOMAIN, [mocksensor], built_in=False) - setup_component(hass, "homeassistant", {}) - setup_component( + await async_setup_component(hass, "homeassistant", {}) + await async_setup_component( hass, "sensor", {"sensor": [{"platform": "demo"}, {"platform": "test"}]} ) - hass.block_till_done() + await hass.async_block_till_done() attributes = { "device_class": device_class, "state_class": state_class, @@ -1390,10 +1425,11 @@ def test_compile_hourly_sum_statistics_negative_state( with freeze_time(zero) as freezer: for i in range(len(seq)): one = one + timedelta(seconds=5) - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, one, entity_id, attributes, seq[i : i + 1] ) states[entity_id].extend(_states[entity_id]) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, @@ -1407,8 +1443,8 @@ def test_compile_hourly_sum_statistics_negative_state( ) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert { "display_unit_of_measurement": display_unit, "has_mean": False, @@ -1462,8 +1498,8 @@ def test_compile_hourly_sum_statistics_negative_state( ("weight", "kg", "kg", "kg", "mass", 1), ], ) -def test_compile_hourly_sum_statistics_total_no_reset( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_total_no_reset( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -1477,9 +1513,9 @@ def test_compile_hourly_sum_statistics_total_no_reset( period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "total", @@ -1487,10 +1523,10 @@ def test_compile_hourly_sum_statistics_total_no_reset( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] with freeze_time(period0) as freezer: - four, eight, states = record_meter_states( + four, eight, states = await async_record_meter_states( hass, freezer, period0, "sensor.test1", attributes, seq ) - wait_recording_done(hass) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, @@ -1502,12 +1538,12 @@ def test_compile_hourly_sum_statistics_total_no_reset( ) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period2) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1575,8 +1611,8 @@ def test_compile_hourly_sum_statistics_total_no_reset( ("weight", "kg", "kg", "kg", "mass", 1), ], ) -def test_compile_hourly_sum_statistics_total_increasing( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_total_increasing( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -1590,9 +1626,9 @@ def test_compile_hourly_sum_statistics_total_increasing( period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "total_increasing", @@ -1600,10 +1636,10 @@ def test_compile_hourly_sum_statistics_total_increasing( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] with freeze_time(period0) as freezer: - four, eight, states = record_meter_states( + four, eight, states = await async_record_meter_states( hass, freezer, period0, "sensor.test1", attributes, seq ) - wait_recording_done(hass) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, @@ -1615,12 +1651,12 @@ def test_compile_hourly_sum_statistics_total_increasing( ) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period2) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1688,8 +1724,8 @@ def test_compile_hourly_sum_statistics_total_increasing( ("weight", "kg", "kg", "kg", "mass", 1), ], ) -def test_compile_hourly_sum_statistics_total_increasing_small_dip( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_sum_statistics_total_increasing_small_dip( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -1703,9 +1739,9 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "total_increasing", @@ -1713,10 +1749,10 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( } seq = [10, 15, 20, 19, 30, 40, 39, 60, 70] with freeze_time(period0) as freezer: - four, eight, states = record_meter_states( + four, eight, states = await async_record_meter_states( hass, freezer, period0, "sensor.test1", attributes, seq ) - wait_recording_done(hass) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, @@ -1728,15 +1764,15 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( ) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " "strictly increasing." ) not in caplog.text do_adhoc_statistics(hass, start=period2) - wait_recording_done(hass) + await async_wait_recording_done(hass) state = states["sensor.test1"][6].state previous_state = float(states["sensor.test1"][5].state) last_updated = states["sensor.test1"][6].last_updated.isoformat() @@ -1746,7 +1782,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( f"last_updated set to {last_updated}. Please create a bug report at " "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" ) in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1797,17 +1833,17 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics_unsupported( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_compile_hourly_energy_statistics_unsupported( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics.""" period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) sns1_attr = { "device_class": "energy", "state_class": "total", @@ -1821,18 +1857,18 @@ def test_compile_hourly_energy_statistics_unsupported( seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] with freeze_time(period0) as freezer: - four, eight, states = record_meter_states( + four, eight, states = await async_record_meter_states( hass, freezer, period0, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_meter_states( + _, _, _states = await async_record_meter_states( hass, freezer, period0, "sensor.test2", sns2_attr, seq2 ) states = {**states, **_states} - _, _, _states = record_meter_states( + _, _, _states = await async_record_meter_states( hass, freezer, period0, "sensor.test3", sns3_attr, seq3 ) states = {**states, **_states} - wait_recording_done(hass) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, @@ -1845,12 +1881,12 @@ def test_compile_hourly_energy_statistics_unsupported( ) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period2) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -1901,17 +1937,17 @@ def test_compile_hourly_energy_statistics_unsupported( assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics_multiple( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_compile_hourly_energy_statistics_multiple( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling multiple hourly statistics.""" period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) sns1_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} sns2_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} sns3_attr = { @@ -1924,18 +1960,18 @@ def test_compile_hourly_energy_statistics_multiple( seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] with freeze_time(period0) as freezer: - four, eight, states = record_meter_states( + four, eight, states = await async_record_meter_states( hass, freezer, period0, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_meter_states( + _, _, _states = await async_record_meter_states( hass, freezer, period0, "sensor.test2", sns2_attr, seq2 ) states = {**states, **_states} - _, _, _states = record_meter_states( + _, _, _states = await async_record_meter_states( hass, freezer, period0, "sensor.test3", sns3_attr, seq3 ) states = {**states, **_states} - wait_recording_done(hass) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, @@ -1947,12 +1983,12 @@ def test_compile_hourly_energy_statistics_multiple( ) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period2) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2111,8 +2147,8 @@ def test_compile_hourly_energy_statistics_multiple( ("weight", "oz", 30), ], ) -def test_compile_hourly_statistics_unchanged( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_unchanged( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2120,23 +2156,26 @@ def test_compile_hourly_statistics_unchanged( ) -> None: """Test compiling hourly statistics, with no changes during the hour.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=four) - wait_recording_done(hass) + await async_wait_recording_done(hass) stats = statistics_during_period(hass, four, period="5minute") assert stats == { "sensor.test1": [ @@ -2155,24 +2194,25 @@ def test_compile_hourly_statistics_unchanged( assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_partially_unavailable( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_compile_hourly_statistics_partially_unavailable( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics, with the sensor being partially unavailable.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added - four, states = record_states_partially_unavailable( + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) + four, states = await async_record_states_partially_unavailable( hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES ) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="5minute") assert stats == { "sensor.test1": [ @@ -2215,8 +2255,8 @@ def test_compile_hourly_statistics_partially_unavailable( ("weight", "oz", 30), ], ) -def test_compile_hourly_statistics_unavailable( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_unavailable( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2228,19 +2268,22 @@ def test_compile_hourly_statistics_unavailable( sensor.test2 should have statistics generated """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } - four, states = record_states_partially_unavailable( + four, states = await async_record_states_partially_unavailable( hass, zero, "sensor.test1", attributes ) with freeze_time(zero) as freezer: - _, _states = record_states(hass, freezer, zero, "sensor.test2", attributes) + _, _states = await async_record_states( + hass, freezer, zero, "sensor.test2", attributes + ) + await async_wait_recording_done(hass) states = {**states, **_states} hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -2248,7 +2291,7 @@ def test_compile_hourly_statistics_unavailable( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=four) - wait_recording_done(hass) + await async_wait_recording_done(hass) stats = statistics_during_period(hass, four, period="5minute") assert stats == { "sensor.test2": [ @@ -2267,20 +2310,20 @@ def test_compile_hourly_statistics_unavailable( assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_fails( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +async def test_compile_hourly_statistics_fails( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics throws.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) with patch( "homeassistant.components.sensor.recorder.compile_statistics", side_effect=Exception, ): do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "Error while processing event StatisticsTask" in caplog.text @@ -2334,8 +2377,8 @@ def test_compile_hourly_statistics_fails( ("total", "weight", "oz", "oz", "oz", "mass", "sum"), ], ) -def test_list_statistic_ids( - hass_recorder: Callable[..., HomeAssistant], +async def test_list_statistic_ids( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, state_class, device_class, @@ -2346,17 +2389,17 @@ def test_list_statistic_ids( statistic_type, ) -> None: """Test listing future statistic ids.""" - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "last_reset": 0, "state_class": state_class, "unit_of_measurement": state_unit, } - hass.states.set("sensor.test1", 0, attributes=attributes) - statistic_ids = list_statistic_ids(hass) + hass.states.async_set("sensor.test1", 0, attributes=attributes) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2370,7 +2413,7 @@ def test_list_statistic_ids( }, ] for stat_type in ["mean", "sum", "dogs"]: - statistic_ids = list_statistic_ids(hass, statistic_type=stat_type) + statistic_ids = await async_list_statistic_ids(hass, statistic_type=stat_type) if statistic_type == stat_type: assert statistic_ids == [ { @@ -2392,31 +2435,31 @@ def test_list_statistic_ids( "_attributes", [{**ENERGY_SENSOR_ATTRIBUTES, "last_reset": 0}, TEMPERATURE_SENSOR_ATTRIBUTES], ) -def test_list_statistic_ids_unsupported( - hass_recorder: Callable[..., HomeAssistant], +async def test_list_statistic_ids_unsupported( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, _attributes, ) -> None: """Test listing future statistic ids for unsupported sensor.""" - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = dict(_attributes) - hass.states.set("sensor.test1", 0, attributes=attributes) + hass.states.async_set("sensor.test1", 0, attributes=attributes) if "last_reset" in attributes: attributes.pop("unit_of_measurement") - hass.states.set("last_reset.test2", 0, attributes=attributes) + hass.states.async_set("last_reset.test2", 0, attributes=attributes) attributes = dict(_attributes) if "unit_of_measurement" in attributes: attributes["unit_of_measurement"] = "invalid" - hass.states.set("sensor.test3", 0, attributes=attributes) + hass.states.async_set("sensor.test3", 0, attributes=attributes) attributes.pop("unit_of_measurement") - hass.states.set("sensor.test4", 0, attributes=attributes) + hass.states.async_set("sensor.test4", 0, attributes=attributes) attributes = dict(_attributes) attributes["state_class"] = "invalid" - hass.states.set("sensor.test5", 0, attributes=attributes) + hass.states.async_set("sensor.test5", 0, attributes=attributes) attributes.pop("state_class") - hass.states.set("sensor.test6", 0, attributes=attributes) + hass.states.async_set("sensor.test6", 0, attributes=attributes) @pytest.mark.parametrize( @@ -2433,8 +2476,8 @@ def test_list_statistic_ids_unsupported( (None, "m³", "m3", "volume", 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_units_1( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_units_1( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2449,34 +2492,37 @@ def test_compile_hourly_statistics_changing_units_1( This tests the case where the recorder cannot convert between the units. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) attributes["unit_of_measurement"] = state_unit2 - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "cannot be converted to the unit of previously" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2506,12 +2552,12 @@ def test_compile_hourly_statistics_changing_units_1( } do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert ( f"The unit of sensor.test1 ({state_unit2}) cannot be converted to the unit of " f"previously compiled statistics ({state_unit})" in caplog.text ) - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2557,8 +2603,8 @@ def test_compile_hourly_statistics_changing_units_1( (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_units_2( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_units_2( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2575,31 +2621,34 @@ def test_compile_hourly_statistics_changing_units_2( converter. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) attributes["unit_of_measurement"] = "cats" - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "The unit of sensor.test1 is changing" in caplog.text assert "and matches the unit of already compiled statistics" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2633,8 +2682,8 @@ def test_compile_hourly_statistics_changing_units_2( (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_units_3( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_units_3( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2651,34 +2700,37 @@ def test_compile_hourly_statistics_changing_units_3( converter. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) - four, _states = record_states( + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] attributes["unit_of_measurement"] = "cats" - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "does not match the unit of already compiled" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2708,12 +2760,12 @@ def test_compile_hourly_statistics_changing_units_3( } do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "The unit of sensor.test1 is changing" in caplog.text assert ( f"matches the unit of already compiled statistics ({state_unit})" in caplog.text ) - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2753,8 +2805,8 @@ def test_compile_hourly_statistics_changing_units_3( ("kW", "W", "power", 13.050847, -10, 30, 1000), ], ) -def test_compile_hourly_statistics_convert_units_1( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_convert_units_1( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, state_unit_1, state_unit_2, @@ -2769,17 +2821,19 @@ def test_compile_hourly_statistics_convert_units_1( This tests the case where the recorder can convert between the units. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": None, "state_class": "measurement", "unit_of_measurement": state_unit_1, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) - four, _states = record_states( + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), @@ -2787,12 +2841,13 @@ def test_compile_hourly_statistics_convert_units_1( attributes, seq=[0, 1, None], ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "does not match the unit of already compiled" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2823,22 +2878,23 @@ def test_compile_hourly_statistics_convert_units_1( attributes["unit_of_measurement"] = state_unit_2 with freeze_time(four) as freezer: - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "The unit of sensor.test1 is changing" not in caplog.text assert ( f"matches the unit of already compiled statistics ({state_unit_1})" not in caplog.text ) - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2909,8 +2965,8 @@ def test_compile_hourly_statistics_convert_units_1( (None, "m3", "m³", None, "volume", 13.050847, 13.333333, -10, 30), ], ) -def test_compile_hourly_statistics_equivalent_units_1( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_equivalent_units_1( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -2924,24 +2980,27 @@ def test_compile_hourly_statistics_equivalent_units_1( ) -> None: """Test compiling hourly statistics where units change from one hour to the next.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) attributes["unit_of_measurement"] = state_unit2 - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -2949,9 +3008,9 @@ def test_compile_hourly_statistics_equivalent_units_1( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "cannot be converted to the unit of previously" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -2981,8 +3040,8 @@ def test_compile_hourly_statistics_equivalent_units_1( } do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3033,8 +3092,8 @@ def test_compile_hourly_statistics_equivalent_units_1( (None, "m3", "m³", None, 13.333333, -10, 30), ], ) -def test_compile_hourly_statistics_equivalent_units_2( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_equivalent_units_2( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -3046,20 +3105,23 @@ def test_compile_hourly_statistics_equivalent_units_2( ) -> None: """Test compiling hourly statistics where units change during an hour.""" zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": device_class, "state_class": "measurement", "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) attributes["unit_of_measurement"] = state_unit2 - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -3067,10 +3129,10 @@ def test_compile_hourly_statistics_equivalent_units_2( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "The unit of sensor.test1 is changing" not in caplog.text assert "and matches the unit of already compiled statistics" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3119,8 +3181,8 @@ def test_compile_hourly_statistics_equivalent_units_2( ("power", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) -def test_compile_hourly_statistics_changing_device_class_1( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_device_class_1( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -3136,9 +3198,9 @@ def test_compile_hourly_statistics_changing_device_class_1( Device class is ignored, meaning changing device class should not influence the statistics. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) # Record some states for an initial period, the entity has no device class attributes = { @@ -3146,12 +3208,15 @@ def test_compile_hourly_statistics_changing_device_class_1( "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "does not match the unit of already compiled" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3183,13 +3248,14 @@ def test_compile_hourly_statistics_changing_device_class_1( # Update device class and record additional states in the original UoM attributes["device_class"] = device_class with freeze_time(zero) as freezer: - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -3198,8 +3264,8 @@ def test_compile_hourly_statistics_changing_device_class_1( # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3241,13 +3307,14 @@ def test_compile_hourly_statistics_changing_device_class_1( # Update device class and record additional states in a different UoM attributes["unit_of_measurement"] = statistic_unit with freeze_time(zero) as freezer: - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=15), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=20), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -3256,8 +3323,8 @@ def test_compile_hourly_statistics_changing_device_class_1( # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=20)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3324,8 +3391,8 @@ def test_compile_hourly_statistics_changing_device_class_1( ("power", "kW", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) -def test_compile_hourly_statistics_changing_device_class_2( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_device_class_2( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -3342,9 +3409,9 @@ def test_compile_hourly_statistics_changing_device_class_2( Device class is ignored, meaning changing device class should not influence the statistics. """ zero = dt_util.utcnow() - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) # Record some states for an initial period, the entity has a device class attributes = { @@ -3353,12 +3420,15 @@ def test_compile_hourly_statistics_changing_device_class_2( "unit_of_measurement": state_unit, } with freeze_time(zero) as freezer: - four, states = record_states(hass, freezer, zero, "sensor.test1", attributes) + four, states = await async_record_states( + hass, freezer, zero, "sensor.test1", attributes + ) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=zero) - wait_recording_done(hass) + await async_wait_recording_done(hass) assert "does not match the unit of already compiled" not in caplog.text - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3390,13 +3460,14 @@ def test_compile_hourly_statistics_changing_device_class_2( # Remove device class and record additional states attributes.pop("device_class") with freeze_time(zero) as freezer: - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, zero + timedelta(minutes=10), "sensor.test1", attributes ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, zero, four, hass.states.async_entity_ids() @@ -3405,8 +3476,8 @@ def test_compile_hourly_statistics_changing_device_class_2( # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3462,8 +3533,8 @@ def test_compile_hourly_statistics_changing_device_class_2( (None, None, None, None, "unitless", 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_state_class( - hass_recorder: Callable[..., HomeAssistant], +async def test_compile_hourly_statistics_changing_state_class( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_class, state_unit, @@ -3478,9 +3549,9 @@ def test_compile_hourly_statistics_changing_state_class( period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period0 + timedelta(minutes=10) - hass = hass_recorder() - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes_1 = { "device_class": device_class, "state_class": "measurement", @@ -3492,12 +3563,13 @@ def test_compile_hourly_statistics_changing_state_class( "unit_of_measurement": state_unit, } with freeze_time(period0) as freezer: - four, states = record_states( + four, states = await async_record_states( hass, freezer, period0, "sensor.test1", attributes_1 ) + await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=period0) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3527,9 +3599,10 @@ def test_compile_hourly_statistics_changing_state_class( # Add more states, with changed state class with freeze_time(period1) as freezer: - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, period1, "sensor.test1", attributes_2 ) + await async_wait_recording_done(hass) states["sensor.test1"] += _states["sensor.test1"] hist = history.get_significant_states( hass, period0, four, hass.states.async_entity_ids() @@ -3537,8 +3610,8 @@ def test_compile_hourly_statistics_changing_state_class( assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=period1) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await async_wait_recording_done(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3595,22 +3668,21 @@ def test_compile_hourly_statistics_changing_state_class( @pytest.mark.timeout(25) -def test_compile_statistics_hourly_daily_monthly_summary( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +@pytest.mark.parametrize("recorder_config", [{CONF_COMMIT_INTERVAL: 3600 * 4}]) +@pytest.mark.freeze_time("2021-09-01 05:00") # August 31st, 23:00 local time +async def test_compile_statistics_hourly_daily_monthly_summary( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, ) -> None: """Test compiling hourly statistics + monthly and daily summary.""" + dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) + zero = dt_util.utcnow() - # August 31st, 23:00 local time - zero = zero.replace( - year=2021, month=9, day=1, hour=5, minute=0, second=0, microsecond=0 - ) - with freeze_time(zero): - hass = hass_recorder() - # Remove this after dropping the use of the hass_recorder fixture - hass.config.set_time_zone("America/Regina") instance = get_instance(hass) - setup_component(hass, "sensor", {}) - wait_recording_done(hass) # Wait for the sensor recorder platform to be added + await async_setup_component(hass, "sensor", {}) + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) attributes = { "device_class": None, "state_class": "measurement", @@ -3673,7 +3745,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( for i in range(24): seq = [-10, 15, 30] # test1 has same value in every period - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, start, "sensor.test1", attributes, seq ) states["sensor.test1"] += _states["sensor.test1"] @@ -3686,7 +3758,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( last_states["sensor.test1"] = seq[-1] # test2 values change: min/max at the last state seq = [-10 * (i + 1), 15 * (i + 1), 30 * (i + 1)] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, start, "sensor.test2", attributes, seq ) states["sensor.test2"] += _states["sensor.test2"] @@ -3699,7 +3771,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( last_states["sensor.test2"] = seq[-1] # test3 values change: min/max at the first state seq = [-10 * (23 - i + 1), 15 * (23 - i + 1), 30 * (23 - i + 1)] - four, _states = record_states( + four, _states = await async_record_states( hass, freezer, start, "sensor.test3", attributes, seq ) states["sensor.test3"] += _states["sensor.test3"] @@ -3714,7 +3786,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( seq = [i, i + 0.5, i + 0.75] start_meter = start for j in range(len(seq)): - _states = record_meter_state( + _states = await async_record_meter_state( hass, freezer, start_meter, @@ -3732,6 +3804,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( last_states["sensor.test4"] = seq[-1] start += timedelta(minutes=5) + await async_wait_recording_done(hass) hist = history.get_significant_states( hass, zero - timedelta.resolution, @@ -3740,16 +3813,16 @@ def test_compile_statistics_hourly_daily_monthly_summary( significant_changes_only=False, ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - wait_recording_done(hass) + await async_wait_recording_done(hass) # Generate 5-minute statistics for two hours start = zero for _ in range(24): do_adhoc_statistics(hass, start=start) - wait_recording_done(hass) + await async_wait_recording_done(hass) start += timedelta(minutes=5) - statistic_ids = list_statistic_ids(hass) + statistic_ids = await async_list_statistic_ids(hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -3801,7 +3874,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( instance.async_adjust_statistics( "sensor.test4", sum_adjustement_start, sum_adjustment, "EUR" ) - wait_recording_done(hass) + await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="5minute") expected_stats = { @@ -4026,7 +4099,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( assert "Error while processing event StatisticsTask" not in caplog.text -def record_states( +async def async_record_states( hass: HomeAssistant, freezer: FrozenDateTimeFactory, zero: datetime, @@ -4044,8 +4117,7 @@ def record_states( def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) return hass.states.get(entity_id) one = zero + timedelta(seconds=1 * 5) @@ -4096,7 +4168,6 @@ def record_states( ], ) async def test_validate_unit_change_convertible( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4218,7 +4289,6 @@ async def test_validate_unit_change_convertible( ], ) async def test_validate_statistics_unit_ignore_device_class( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4306,7 +4376,6 @@ async def test_validate_statistics_unit_ignore_device_class( ], ) async def test_validate_statistics_unit_change_no_device_class( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4428,7 +4497,6 @@ async def test_validate_statistics_unit_change_no_device_class( ], ) async def test_validate_statistics_unsupported_state_class( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4497,7 +4565,6 @@ async def test_validate_statistics_unsupported_state_class( ], ) async def test_validate_statistics_sensor_no_longer_recorded( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4565,7 +4632,6 @@ async def test_validate_statistics_sensor_no_longer_recorded( ], ) async def test_validate_statistics_sensor_not_recorded( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4630,7 +4696,6 @@ async def test_validate_statistics_sensor_not_recorded( ], ) async def test_validate_statistics_sensor_removed( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, units, @@ -4694,7 +4759,6 @@ async def test_validate_statistics_sensor_removed( ], ) async def test_validate_statistics_unit_change_no_conversion( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, attributes, @@ -4825,7 +4889,6 @@ async def test_validate_statistics_unit_change_no_conversion( ], ) async def test_validate_statistics_unit_change_equivalent_units( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, attributes, @@ -4909,7 +4972,6 @@ async def test_validate_statistics_unit_change_equivalent_units( ], ) async def test_validate_statistics_unit_change_equivalent_units_2( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, attributes, @@ -5002,7 +5064,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( async def test_validate_statistics_other_domain( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test sensor does not raise issues for statistics for other domains.""" msg_id = 1 @@ -5049,7 +5111,7 @@ async def test_validate_statistics_other_domain( await assert_validation_result(client, {}) -def record_meter_states( +async def async_record_meter_states( hass: HomeAssistant, freezer: FrozenDateTimeFactory, zero: datetime, @@ -5064,7 +5126,7 @@ def record_meter_states( def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) + hass.states.async_set(entity_id, state, **kwargs) return hass.states.get(entity_id) one = zero + timedelta(seconds=15 * 5) # 00:01:15 @@ -5116,7 +5178,7 @@ def record_meter_states( return four, eight, states -def record_meter_state( +async def async_record_meter_state( hass: HomeAssistant, freezer: FrozenDateTimeFactory, zero: datetime, @@ -5131,8 +5193,7 @@ def record_meter_state( def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) return hass.states.get(entity_id) states = {entity_id: []} @@ -5142,7 +5203,7 @@ def record_meter_state( return states -def record_states_partially_unavailable(hass, zero, entity_id, attributes): +async def async_record_states_partially_unavailable(hass, zero, entity_id, attributes): """Record some test states. We inject a bunch of state updates temperature sensors. @@ -5150,8 +5211,7 @@ def record_states_partially_unavailable(hass, zero, entity_id, attributes): def set_state(entity_id, state, **kwargs): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) + hass.states.async_set(entity_id, state, **kwargs) return hass.states.get(entity_id) one = zero + timedelta(seconds=1 * 5) @@ -5175,7 +5235,7 @@ def record_states_partially_unavailable(hass, zero, entity_id, attributes): async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, enable_custom_integrations: None ) -> None: """Test sensor attributes to be excluded.""" entity0 = MockSensor( diff --git a/tests/components/snapcast/conftest.py b/tests/components/snapcast/conftest.py index 7d29b098482..9e3325bd73a 100644 --- a/tests/components/snapcast/conftest.py +++ b/tests/components/snapcast/conftest.py @@ -1,7 +1,7 @@ """Test the snapcast config flow.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -20,5 +20,6 @@ def mock_create_server() -> Generator[AsyncMock, None, None]: """Create mock snapcast connection.""" mock_connection = AsyncMock() mock_connection.start = AsyncMock(return_value=None) + mock_connection.stop = MagicMock() with patch("snapcast.control.create_server", return_value=mock_connection): yield mock_connection diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index f509c91ad20..883f60aaf0a 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock from homeassistant import config_entries -from homeassistant.components import speedtestdotnet from homeassistant.components.speedtestdotnet.const import ( CONF_SERVER_ID, CONF_SERVER_NAME, @@ -18,7 +17,7 @@ from tests.common import MockConfigEntry async def test_flow_works(hass: HomeAssistant) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -84,7 +83,7 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 446ed527df4..2e20aaa259c 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -10,6 +10,9 @@ from homeassistant.components.speedtestdotnet.const import ( CONF_SERVER_NAME, DOMAIN, ) +from homeassistant.components.speedtestdotnet.coordinator import ( + SpeedTestDataCoordinator, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -47,13 +50,12 @@ async def test_entry_lifecycle(hass: HomeAssistant, mock_api: MagicMock) -> None await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - assert hass.data[DOMAIN] + assert isinstance(entry.runtime_data, SpeedTestDataCoordinator) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED - assert DOMAIN not in hass.data async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -67,7 +69,9 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] + + assert entry.state is ConfigEntryState.LOADED + assert isinstance(entry.runtime_data, SpeedTestDataCoordinator) mock_api.return_value.get_servers.side_effect = speedtest.NoMatchedServers async_fire_time_changed( @@ -90,14 +94,16 @@ async def test_get_best_server_error(hass: HomeAssistant, mock_api: MagicMock) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] + + assert entry.state is ConfigEntryState.LOADED + assert isinstance(entry.runtime_data, SpeedTestDataCoordinator) mock_api.return_value.get_best_server.side_effect = ( speedtest.SpeedtestBestServerFailure( "Unable to connect to servers to test latency." ) ) - await hass.data[DOMAIN].async_refresh() + await entry.runtime_data.async_refresh() await hass.async_block_till_done() state = hass.states.get("sensor.speedtest_ping") assert state is not None diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index e529d46b537..a14a482b66f 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.speedtestdotnet import DOMAIN +from homeassistant.components.speedtestdotnet.const import DOMAIN from homeassistant.core import HomeAssistant from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index a5adab4c77f..c824a16d952 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -70,6 +70,7 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("AA:BB:CC:DD:EE:FF", "WoHand"), time=0, connectable=True, + tx_power=-127, ) @@ -90,6 +91,7 @@ WOHAND_SERVICE_INFO_NOT_CONNECTABLE = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "WoHand"), time=0, connectable=False, + tx_power=-127, ) @@ -110,6 +112,7 @@ WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("798A8547-2A3D-C609-55FF-73FA824B923B", "WoHand"), time=0, connectable=True, + tx_power=-127, ) @@ -130,6 +133,7 @@ WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "WoHand"), time=0, connectable=True, + tx_power=-127, ) WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoCurtain", @@ -148,6 +152,7 @@ WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "WoCurtain"), time=0, connectable=True, + tx_power=-127, ) WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -165,6 +170,7 @@ WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "WoSensorTH"), time=0, connectable=False, + tx_power=-127, ) @@ -185,6 +191,7 @@ WOLOCK_SERVICE_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "WoLock"), time=0, connectable=True, + tx_power=-127, ) NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( @@ -202,4 +209,5 @@ NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( device=generate_ble_device("aa:bb:cc:dd:ee:ff", "unknown"), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 176fe54c34a..a4f8333e8a8 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -5,14 +5,20 @@ from unittest.mock import patch import pytest from syrupy import SnapshotAssertion -from total_connect_client.exceptions import ServiceUnavailable, TotalConnectError +from total_connect_client.exceptions import ( + AuthenticationError, + ServiceUnavailable, + TotalConnectError, +) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN -from homeassistant.components.totalconnect import DOMAIN, SCAN_INTERVAL from homeassistant.components.totalconnect.alarm_control_panel import ( SERVICE_ALARM_ARM_AWAY_INSTANT, SERVICE_ALARM_ARM_HOME_INSTANT, ) +from homeassistant.components.totalconnect.const import DOMAIN +from homeassistant.components.totalconnect.coordinator import SCAN_INTERVAL +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_ALARM_ARM_AWAY, @@ -566,3 +572,25 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 6 + + +async def test_authentication_error(hass: HomeAssistant) -> None: + """Test other failures seen during updates.""" + entry = await setup_platform(hass, ALARM_DOMAIN) + + with patch(TOTALCONNECT_REQUEST, side_effect=AuthenticationError): + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 40bd965fd9d..d43c317e772 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -16,6 +16,7 @@ NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -34,6 +35,7 @@ LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -52,6 +54,7 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -70,6 +73,7 @@ JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -88,6 +92,7 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) HHCCJCY10_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -102,6 +107,7 @@ HHCCJCY10_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) MISCALE_V1_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -118,6 +124,7 @@ MISCALE_V1_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) MISCALE_V2_SERVICE_INFO = BluetoothServiceInfoBleak( @@ -134,6 +141,7 @@ MISCALE_V2_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( @@ -150,6 +158,7 @@ MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(local_name="Not it"), time=0, connectable=False, + tx_power=-127, ) @@ -171,4 +180,5 @@ def make_advertisement( advertisement=generate_advertisement_data(local_name="Test Device"), time=0, connectable=connectable, + tx_power=-127, ) diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py index 62a702f2f41..d6ce326cbe2 100644 --- a/tests/components/yalexs_ble/__init__.py +++ b/tests/components/yalexs_ble/__init__.py @@ -19,6 +19,7 @@ YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) @@ -37,6 +38,7 @@ LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( @@ -54,6 +56,7 @@ OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) @@ -72,4 +75,5 @@ NOT_YALE_DISCOVERY_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data(), time=0, connectable=True, + tx_power=-127, ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 07228abcc2c..a6fad968eac 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4819,3 +4819,21 @@ async def test_track_state_change_deprecated( "of `async_track_state_change_event` which is deprecated and " "will be removed in Home Assistant 2025.5. Please report this issue." ) in caplog.text + + +async def test_track_point_in_time_repr( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test track point in time.""" + + @ha.callback + def _raise_exception(_): + raise RuntimeError("something happened and its poorly described") + + async_track_point_in_utc_time(hass, _raise_exception, dt_util.utcnow()) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Exception in callback _TrackPointUTCTime" in caplog.text + assert "._raise_exception" in caplog.text + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 3be2093057b..aaf6cbe3efe 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -7,6 +7,12 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ "tests.test_runner", "test_unhandled_exception_traceback", ), + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.helpers.test_event", + "test_track_point_in_time_repr", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup",