diff --git a/.coveragerc b/.coveragerc index 1ccb9e461df..10dedd43e81 100644 --- a/.coveragerc +++ b/.coveragerc @@ -939,6 +939,7 @@ omit = homeassistant/components/omnilogic/switch.py homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/api.py + homeassistant/components/ondilo_ico/coordinator.py homeassistant/components/ondilo_ico/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index b4a0f237522..418e8997239 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -4,30 +4,35 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN from .hub import PulseHub CONF_HUBS = "hubs" PLATFORMS = [Platform.COVER, Platform.SENSOR] +AcmedaConfigEntry = ConfigEntry[PulseHub] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: AcmedaConfigEntry +) -> bool: """Set up Rollease Acmeda Automate hub from a config entry.""" hub = PulseHub(hass, config_entry) if not await hub.async_setup(): return False - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + config_entry.runtime_data = hub 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: AcmedaConfigEntry +) -> bool: """Unload a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub = config_entry.runtime_data unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS @@ -36,7 +41,4 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if not await hub.async_reset(): return False - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - return unload_ok diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index f8116221668..d96675de10c 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -9,24 +9,23 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AcmedaConfigEntry from .base import AcmedaBase -from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .const import ACMEDA_HUB_UPDATE from .helpers import async_add_acmeda_entities -from .hub import PulseHub async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AcmedaConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] + hub = config_entry.runtime_data current: set[int] = set() diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index 9e48124208a..52af7d586de 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from aiopulse import Roller from homeassistant.config_entries import ConfigEntry @@ -11,17 +13,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER +if TYPE_CHECKING: + from . import AcmedaConfigEntry + @callback def async_add_acmeda_entities( hass: HomeAssistant, entity_class: type, - config_entry: ConfigEntry, + config_entry: AcmedaConfigEntry, current: set[int], async_add_entities: AddEntitiesCallback, ) -> None: """Add any new entities.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub = config_entry.runtime_data LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) api = hub.api.rollers diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 0b458a8c32a..be9f37b03dc 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -3,25 +3,24 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AcmedaConfigEntry from .base import AcmedaBase -from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .const import ACMEDA_HUB_UPDATE from .helpers import async_add_acmeda_entities -from .hub import PulseHub async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AcmedaConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] + hub = config_entry.runtime_data current: set[int] = set() diff --git a/homeassistant/components/aftership/__init__.py b/homeassistant/components/aftership/__init__.py index b079079db08..10e4293bc51 100644 --- a/homeassistant/components/aftership/__init__.py +++ b/homeassistant/components/aftership/__init__.py @@ -10,16 +10,14 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN - PLATFORMS: list[Platform] = [Platform.SENSOR] +AfterShipConfigEntry = ConfigEntry[AfterShip] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool: """Set up AfterShip from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - session = async_get_clientsession(hass) aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session) @@ -28,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AfterShipException as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id] = aftership + entry.runtime_data = aftership await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -37,7 +35,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/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index c403c4a571d..c019634197d 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -8,7 +8,6 @@ from typing import Any, Final from pyaftership import AfterShip, AfterShipException from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -18,6 +17,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle +from . import AfterShipConfigEntry from .const import ( ADD_TRACKING_SERVICE_SCHEMA, ATTR_TRACKINGS, @@ -41,11 +41,11 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AfterShipConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up AfterShip sensor entities based on a config entry.""" - aftership: AfterShip = hass.data[DOMAIN][config_entry.entry_id] + aftership = config_entry.runtime_data async_add_entities([AfterShipSensor(aftership, config_entry.title)], True) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 8fba13164e7..5b06a25f13a 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -15,14 +15,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import DOMAIN # noqa: F401 from .coordinator import AirNowDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] +AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool: """Set up AirNow from a config entry.""" api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -44,8 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Store Entity and Initialize Platforms - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator # Listen for option changes entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -87,14 +88,9 @@ async def async_migrate_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: AirNowConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/airnow/diagnostics.py b/homeassistant/components/airnow/diagnostics.py index 39db915bef9..76cc35fb13c 100644 --- a/homeassistant/components/airnow/diagnostics.py +++ b/homeassistant/components/airnow/diagnostics.py @@ -5,7 +5,6 @@ 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_API_KEY, CONF_LATITUDE, @@ -14,8 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import AirNowDataUpdateCoordinator -from .const import DOMAIN +from . import AirNowConfigEntry ATTR_LATITUDE_CAP = "Latitude" ATTR_LONGITUDE_CAP = "Longitude" @@ -40,10 +38,10 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: AirNowConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: AirNowDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data return async_redact_data( { diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 1289b6c2b16..559478a69d3 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TIME, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -26,7 +25,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import get_time_zone -from . import AirNowDataUpdateCoordinator +from . import AirNowConfigEntry, AirNowDataUpdateCoordinator from .const import ( ATTR_API_AQI, ATTR_API_AQI_DESCRIPTION, @@ -116,11 +115,11 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AirNowConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up AirNow sensor entities based on a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES] diff --git a/homeassistant/components/airq/__init__.py b/homeassistant/components/airq/__init__.py index dc35cd6ae87..219a72042ef 100644 --- a/homeassistant/components/airq/__init__.py +++ b/homeassistant/components/airq/__init__.py @@ -6,13 +6,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN from .coordinator import AirQCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] +AirQConfigEntry = ConfigEntry[AirQCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool: """Set up air-Q from a config entry.""" coordinator = AirQCoordinator(hass, entry) @@ -20,18 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Query the device for the first time and initialise coordinator.data await coordinator.async_config_entry_first_refresh() - # Record the coordinator in a global store - 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) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> 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/airq/sensor.py b/homeassistant/components/airq/sensor.py index e3ef6504731..c465d710406 100644 --- a/homeassistant/components/airq/sensor.py +++ b/homeassistant/components/airq/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, @@ -28,11 +27,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AirQCoordinator +from . import AirQConfigEntry, AirQCoordinator from .const import ( ACTIVITY_BECQUEREL_PER_CUBIC_METER, CONCENTRATION_GRAMS_PER_CUBIC_METER, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -400,12 +398,12 @@ SENSOR_TYPES: list[AirQEntityDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config: ConfigEntry, + entry: AirQConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor entities based on a config entry.""" - coordinator = hass.data[DOMAIN][config.entry_id] + coordinator = entry.runtime_data entities: list[AirQSensor] = [] diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index bc12f19a33d..c2c4e452730 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -22,11 +22,11 @@ SCAN_INTERVAL = timedelta(minutes=6) AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]] +AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: """Set up Airthings from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - airthings = Airthings( entry.data[CONF_ID], entry.data[CONF_SECRET], @@ -49,17 +49,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> 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) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index f0a3dc5be8f..74d712ccfc6 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -10,7 +10,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_BILLION, @@ -27,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AirthingsDataCoordinatorType +from . import AirthingsConfigEntry, AirthingsDataCoordinatorType from .const import DOMAIN SENSORS: dict[str, SensorEntityDescription] = { @@ -102,12 +101,12 @@ SENSORS: dict[str, SensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: AirthingsConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airthings sensor.""" - coordinator: AirthingsDataCoordinatorType = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data entities = [ AirthingsHeaterEnergySensor( coordinator, diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index 39617a8a019..a1053f6856e 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -22,8 +22,13 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice] +AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: AirthingsBLEConfigEntry +) -> bool: """Set up Airthings BLE device from a config entry.""" hass.data.setdefault(DOMAIN, {}) address = entry.unique_id @@ -51,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data - coordinator = DataUpdateCoordinator( + coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, @@ -61,16 +66,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: AirthingsBLEConfigEntry +) -> 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/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 3f7bd02a33e..d93e3a0b8cb 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.7.1"] + "requirements": ["airthings-ble==0.8.0"] } diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 3b012ed7316..2883c2b351e 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, @@ -36,12 +35,10 @@ from homeassistant.helpers.entity_registry import ( async_get as entity_async_get, ) from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.unit_system import METRIC_SYSTEM +from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE _LOGGER = logging.getLogger(__name__) @@ -152,15 +149,13 @@ def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: AirthingsBLEConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airthings BLE sensors.""" is_metric = hass.config.units is METRIC_SYSTEM - coordinator: DataUpdateCoordinator[AirthingsDevice] = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data # we need to change some units sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy() @@ -193,7 +188,7 @@ async def async_setup_entry( class AirthingsSensor( - CoordinatorEntity[DataUpdateCoordinator[AirthingsDevice]], SensorEntity + CoordinatorEntity[AirthingsBLEDataUpdateCoordinator], SensorEntity ): """Airthings BLE sensors for the device.""" @@ -201,7 +196,7 @@ class AirthingsSensor( def __init__( self, - coordinator: DataUpdateCoordinator[AirthingsDevice], + coordinator: AirthingsBLEDataUpdateCoordinator, airthings_device: AirthingsDevice, entity_description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/airtouch5/__init__.py b/homeassistant/components/airtouch5/__init__.py index b8b9a3f765a..4ae6c1f1fee 100644 --- a/homeassistant/components/airtouch5/__init__.py +++ b/homeassistant/components/airtouch5/__init__.py @@ -13,8 +13,10 @@ from .const import DOMAIN PLATFORMS: list[Platform] = [Platform.CLIMATE] +Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool: """Set up Airtouch 5 from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -30,22 +32,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from t # Store an API object for your platforms to access - hass.data[DOMAIN][entry.entry_id] = client + entry.runtime_data = client 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: Airtouch5ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - client: Airtouch5SimpleClient = hass.data[DOMAIN][entry.entry_id] + client = entry.runtime_data await client.disconnect() client.ac_status_callbacks.clear() client.connection_state_callbacks.clear() client.data_packet_callbacks.clear() client.zone_status_callbacks.clear() - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index 157e3b7d643..1f97c254efe 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -34,12 +34,12 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import Airtouch5ConfigEntry from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO from .entity import Airtouch5Entity @@ -92,11 +92,11 @@ FAN_MODE_TO_SET_AC_FAN_SPEED = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: Airtouch5ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airtouch 5 Climate entities.""" - client: Airtouch5SimpleClient = hass.data[DOMAIN][config_entry.entry_id] + client = config_entry.runtime_data entities: list[ClimateEntity] = [] diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index 88f05d28145..a02e735a5d6 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -38,6 +38,8 @@ PLATFORMS = [Platform.SENSOR] UPDATE_INTERVAL = timedelta(minutes=1) +AirVisualProConfigEntry = ConfigEntry["AirVisualProData"] + @dataclass class AirVisualProData: @@ -47,7 +49,9 @@ class AirVisualProData: node: NodeSamba -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: AirVisualProConfigEntry +) -> bool: """Set up AirVisual Pro from a config entry.""" node = NodeSamba(entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]) @@ -89,9 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AirVisualProData( - coordinator=coordinator, node=node - ) + entry.runtime_data = AirVisualProData(coordinator=coordinator, node=node) async def async_shutdown(_: Event) -> None: """Define an event handler to disconnect from the websocket.""" @@ -110,11 +112,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: AirVisualProConfigEntry +) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - data = hass.data[DOMAIN].pop(entry.entry_id) - await data.node.async_disconnect() + await entry.runtime_data.node.async_disconnect() return unload_ok diff --git a/homeassistant/components/airvisual_pro/diagnostics.py b/homeassistant/components/airvisual_pro/diagnostics.py index 9fea6e59c1d..da871442547 100644 --- a/homeassistant/components/airvisual_pro/diagnostics.py +++ b/homeassistant/components/airvisual_pro/diagnostics.py @@ -5,12 +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_PASSWORD from homeassistant.core import HomeAssistant -from . import AirVisualProData -from .const import DOMAIN +from . import AirVisualProConfigEntry CONF_MAC_ADDRESS = "mac_address" CONF_SERIAL_NUMBER = "serial_number" @@ -23,15 +21,13 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: AirVisualProConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: AirVisualProData = hass.data[DOMAIN][entry.entry_id] - return async_redact_data( { "entry": entry.as_dict(), - "data": data.coordinator.data, + "data": entry.runtime_data.coordinator.data, }, TO_REDACT, ) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index d53def57959..895ba7d3244 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -12,7 +12,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, @@ -23,8 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirVisualProData, AirVisualProEntity -from .const import DOMAIN +from . import AirVisualProConfigEntry, AirVisualProEntity @dataclass(frozen=True, kw_only=True) @@ -129,13 +127,13 @@ def async_get_aqi_locale(settings: dict[str, Any]) -> str: async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirVisualProConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up AirVisual sensors based on a config entry.""" - data: AirVisualProData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - AirVisualProSensor(data.coordinator, description) + AirVisualProSensor(entry.runtime_data.coordinator, description) for description in SENSOR_DESCRIPTIONS ) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index b55a7b866cc..39586f4dbf4 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -39,6 +39,8 @@ DEFAULT_SOCKET_MIN_RETRY = 15 CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +AmbientStationConfigEntry = ConfigEntry["AmbientStation"] + @callback def async_wm2_to_lx(value: float) -> int: @@ -55,7 +57,9 @@ def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]: return data -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: AmbientStationConfigEntry +) -> bool: """Set up the Ambient PWS as config entry.""" if not entry.unique_id: hass.config_entries.async_update_entry( @@ -74,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ambient + entry.runtime_data = ambient async def _async_disconnect_websocket(_: Event) -> None: await ambient.websocket.disconnect() @@ -88,12 +92,13 @@ 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: AmbientStationConfigEntry +) -> bool: """Unload an Ambient PWS config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - ambient = hass.data[DOMAIN].pop(entry.entry_id) - hass.async_create_task(ambient.ws_disconnect(), eager_start=True) + hass.async_create_task(entry.runtime_data.ws_disconnect(), eager_start=True) return unload_ok diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index fc21455a00f..a79788a4c38 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -10,12 +10,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_LAST_DATA, DOMAIN +from . import AmbientStationConfigEntry +from .const import ATTR_LAST_DATA from .entity import AmbientWeatherEntity TYPE_BATT1 = "batt1" @@ -379,10 +379,12 @@ BINARY_SENSOR_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AmbientStationConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Ambient PWS binary sensors based on a config entry.""" - ambient = hass.data[DOMAIN][entry.entry_id] + ambient = entry.runtime_data async_add_entities( AmbientWeatherBinarySensor( diff --git a/homeassistant/components/ambient_station/diagnostics.py b/homeassistant/components/ambient_station/diagnostics.py index f3508b8df38..bddbb1ab9df 100644 --- a/homeassistant/components/ambient_station/diagnostics.py +++ b/homeassistant/components/ambient_station/diagnostics.py @@ -5,12 +5,11 @@ 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_API_KEY, CONF_LOCATION, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from . import AmbientStation -from .const import CONF_APP_KEY, DOMAIN +from . import AmbientStationConfigEntry +from .const import CONF_APP_KEY CONF_API_KEY_CAMEL = "apiKey" CONF_APP_KEY_CAMEL = "appKey" @@ -37,12 +36,10 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: AmbientStationConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - ambient: AmbientStation = hass.data[DOMAIN][entry.entry_id] - return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "stations": async_redact_data(ambient.stations, TO_REDACT), + "stations": async_redact_data(entry.runtime_data.stations, TO_REDACT), } diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 229ebee4fbf..dfbd2d1b4a0 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -30,8 +29,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AmbientStation -from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX +from . import AmbientStation, AmbientStationConfigEntry +from .const import ATTR_LAST_DATA, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX from .entity import AmbientWeatherEntity TYPE_24HOURRAININ = "24hourrainin" @@ -661,10 +660,12 @@ SENSOR_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AmbientStationConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Ambient PWS sensors based on a config entry.""" - ambient = hass.data[DOMAIN][entry.entry_id] + ambient = entry.runtime_data async_add_entities( AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 2db2b173f6b..40dc59ae90a 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -49,6 +49,8 @@ API_CACHED_ATTRS = { } YALEXS_BLE_DOMAIN = "yalexs_ble" +AugustConfigEntry = ConfigEntry["AugustData"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" @@ -66,22 +68,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Unload a config entry.""" - - data: AugustData = hass.data[DOMAIN][entry.entry_id] - data.async_stop() - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + entry.runtime_data.async_stop() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_setup_august( - hass: HomeAssistant, config_entry: ConfigEntry, august_gateway: AugustGateway + hass: HomeAssistant, config_entry: AugustConfigEntry, august_gateway: AugustGateway ) -> bool: """Set up the August component.""" @@ -95,10 +89,7 @@ async def async_setup_august( await august_gateway.async_authenticate() await august_gateway.async_refresh_access_token_if_needed() - hass.data.setdefault(DOMAIN, {}) - data = hass.data[DOMAIN][config_entry.entry_id] = AugustData( - hass, config_entry, august_gateway - ) + data = config_entry.runtime_data = AugustData(hass, config_entry, august_gateway) await data.async_setup() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -509,12 +500,12 @@ def _restore_live_attrs( async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, config_entry: AugustConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove august config entry from a device if its no longer present.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] return not any( identifier for identifier in device_entry.identifiers - if identifier[0] == DOMAIN and data.get_device(identifier[1]) + if identifier[0] == DOMAIN + and config_entry.runtime_data.get_device(identifier[1]) ) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 14b9dca9b7d..baf78bbd445 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -22,14 +22,13 @@ 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.event import async_call_later -from . import AugustData -from .const import ACTIVITY_UPDATE_INTERVAL, DOMAIN +from . import AugustConfigEntry, AugustData +from .const import ACTIVITY_UPDATE_INTERVAL from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -154,11 +153,11 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AugustConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August binary sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data entities: list[BinarySensorEntity] = [] for door in data.locks: diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index 579f0012223..d7aefca5d3c 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -3,22 +3,20 @@ from yalexs.lock import Lock from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AugustData -from .const import DOMAIN +from . import AugustConfigEntry, AugustData from .entity import AugustEntityMixin async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AugustConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up August lock wake buttons.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data async_add_entities(AugustWakeLockButton(data, lock) for lock in data.locks) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 188a55bd4b9..4c56502e6c7 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -11,13 +11,12 @@ from yalexs.doorbell import ContentTokenExpired, Doorbell from yalexs.util import update_doorbell_image_from_activity from homeassistant.components.camera import Camera -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AugustData -from .const import DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN +from . import AugustConfigEntry, AugustData +from .const import DEFAULT_NAME, DEFAULT_TIMEOUT from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -25,11 +24,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AugustConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up August cameras.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data # Create an aiohttp session instead of using the default one since the # default one is likely to trigger august's WAF if another integration # is also using Cloudflare diff --git a/homeassistant/components/august/diagnostics.py b/homeassistant/components/august/diagnostics.py index a1f76bf690b..b061e224df9 100644 --- a/homeassistant/components/august/diagnostics.py +++ b/homeassistant/components/august/diagnostics.py @@ -7,11 +7,10 @@ from typing import Any from yalexs.const import DEFAULT_BRAND from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import AugustData -from .const import CONF_BRAND, DOMAIN +from . import AugustConfigEntry +from .const import CONF_BRAND TO_REDACT = { "HouseID", @@ -30,10 +29,10 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: AugustConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: AugustData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data return { "locks": { diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index a6b549b8c89..5a07a5de272 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -12,15 +12,13 @@ from yalexs.lock import Lock, LockStatus from yalexs.util import get_latest_activity, update_lock_detail_from_activity from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util -from . import AugustData -from .const import DOMAIN +from . import AugustConfigEntry, AugustData from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -30,11 +28,11 @@ LOCK_JAMMED_ERR = 531 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AugustConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up August locks.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data async_add_entities(AugustLock(data, lock) for lock in data.locks) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 6ccdccfce7d..c1dc6620f81 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -19,7 +19,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_PICTURE, PERCENTAGE, @@ -30,7 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AugustData +from . import AugustConfigEntry, AugustData from .const import ( ATTR_OPERATION_AUTORELOCK, ATTR_OPERATION_KEYPAD, @@ -95,11 +94,11 @@ SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AugustConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data entities: list[SensorEntity] = [] migrate_unique_id_devices = [] operation_sensors = [] diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index acc38cad58b..49fadd1892e 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -51,8 +51,9 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import async_delete_issue from homeassistant.loader import async_get_bluetooth -from . import models, passive_update_processor +from . import passive_update_processor from .api import ( + _get_manager, async_address_present, async_ble_device_from_address, async_discovered_service_info, @@ -76,7 +77,6 @@ from .const import ( CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, - DATA_MANAGER, DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, @@ -230,10 +230,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager ) set_manager(manager) - await storage_setup_task await manager.async_setup() - hass.data[DATA_MANAGER] = models.MANAGER = manager hass.async_create_background_task( _async_start_adapter_discovery(hass, manager, bluetooth_adapters), @@ -314,7 +312,7 @@ async def async_update_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for a bluetooth scanner.""" - manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] + manager = _get_manager(hass) address = entry.unique_id assert address is not None adapter = await manager.async_get_adapter_from_address_or_recover(address) diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index b1a6bc87728..505651edafd 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -15,10 +15,12 @@ from habluetooth import ( BluetoothScannerDevice, BluetoothScanningMode, HaBleakScannerWrapper, + get_manager, ) from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback +from homeassistant.helpers.singleton import singleton from .const import DATA_MANAGER from .manager import HomeAssistantBluetoothManager @@ -29,9 +31,10 @@ if TYPE_CHECKING: from bleak.backends.device import BLEDevice +@singleton(DATA_MANAGER) def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager: """Get the bluetooth manager.""" - return cast(HomeAssistantBluetoothManager, hass.data[DATA_MANAGER]) + return cast(HomeAssistantBluetoothManager, get_manager()) @hass_callback @@ -68,8 +71,6 @@ def async_discovered_service_info( hass: HomeAssistant, connectable: bool = True ) -> Iterable[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" - if DATA_MANAGER not in hass.data: - return [] return _get_manager(hass).async_discovered_service_info(connectable) @@ -78,8 +79,6 @@ def async_last_service_info( hass: HomeAssistant, address: str, connectable: bool = True ) -> BluetoothServiceInfoBleak | None: """Return the last service info for an address.""" - if DATA_MANAGER not in hass.data: - return None return _get_manager(hass).async_last_service_info(address, connectable) @@ -88,8 +87,6 @@ def async_ble_device_from_address( hass: HomeAssistant, address: str, connectable: bool = True ) -> BLEDevice | None: """Return BLEDevice for an address if its present.""" - if DATA_MANAGER not in hass.data: - return None return _get_manager(hass).async_ble_device_from_address(address, connectable) @@ -106,8 +103,6 @@ def async_address_present( hass: HomeAssistant, address: str, connectable: bool = True ) -> bool: """Check if an address is present in the bluetooth device list.""" - if DATA_MANAGER not in hass.data: - return False return _get_manager(hass).async_address_present(address, connectable) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 90d2624fb0f..37eefd2f265 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -14,6 +14,7 @@ from bluetooth_adapters import ( adapter_model, get_adapters, ) +from habluetooth import get_manager import voluptuous as vol from homeassistant.components import onboarding @@ -25,7 +26,6 @@ from homeassistant.helpers.schema_config_entry_flow import ( ) from homeassistant.helpers.typing import DiscoveryInfoType -from . import models from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN from .util import adapter_title @@ -185,4 +185,4 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @callback def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" - return bool(models.MANAGER and models.MANAGER.supports_passive_scan) + return bool((manager := get_manager()) and manager.supports_passive_scan) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 4bb84ab6dc3..754e8faf996 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.8.0" + "habluetooth==2.8.1" ] } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a14aaf1d379..a97056e1f4b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,17 +4,9 @@ from __future__ import annotations from collections.abc import Callable from enum import Enum -from typing import TYPE_CHECKING from home_assistant_bluetooth import BluetoothServiceInfoBleak -if TYPE_CHECKING: - from .manager import HomeAssistantBluetoothManager - - -MANAGER: HomeAssistantBluetoothManager | None = None - - BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 9ecfedee570..d534e10b023 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -35,8 +35,10 @@ _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 _LOGGER = logging.getLogger(__name__) +BondConfigEntry = ConfigEntry[BondData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool: """Set up Bond from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_ACCESS_TOKEN] @@ -70,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BondData(hub, bpup_subs) + entry.runtime_data = BondData(hub, bpup_subs) if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) @@ -97,11 +99,9 @@ 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: BondConfigEntry) -> 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) @callback @@ -118,10 +118,10 @@ def _async_remove_old_device_identifiers( async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, config_entry: BondConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove bond config entry from a device.""" - data: BondData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data hub = data.hub for identifier in device_entry.identifiers: if identifier[0] != DOMAIN or len(identifier) != 3: diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index a8a5a890f2c..4e243198e5e 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -7,13 +7,11 @@ from dataclasses import dataclass from bond_async import Action, BPUPSubscriptions from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import BondConfigEntry from .entity import BondEntity -from .models import BondData from .utils import BondDevice, BondHub # The api requires a step size even though it does not @@ -243,11 +241,11 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BondConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond button devices.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub bpup_subs = data.bpup_subs entities: list[BondButtonEntity] = [] diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 06576277520..c576972bf26 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -12,13 +12,11 @@ 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 BondConfigEntry from .entity import BondEntity -from .models import BondData from .utils import BondDevice, BondHub @@ -34,11 +32,11 @@ def _hass_to_bond_position(hass_position: int) -> int: async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BondConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond cover devices.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub bpup_subs = data.bpup_subs diff --git a/homeassistant/components/bond/diagnostics.py b/homeassistant/components/bond/diagnostics.py index 8b79f36dd0b..212df43a450 100644 --- a/homeassistant/components/bond/diagnostics.py +++ b/homeassistant/components/bond/diagnostics.py @@ -5,20 +5,18 @@ from __future__ import annotations 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 .models import BondData +from . import BondConfigEntry TO_REDACT = {"access_token"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: BondConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub return { "entry": { diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 1b7a06fcd37..4ed6f83a980 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -16,7 +16,6 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -27,9 +26,9 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range -from .const import DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE +from . import BondConfigEntry +from .const import SERVICE_SET_FAN_SPEED_TRACKED_STATE from .entity import BondEntity -from .models import BondData from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) @@ -39,11 +38,11 @@ PRESET_MODE_BREEZE = "Breeze" async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BondConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond fan devices.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index bd1183a3a98..8ad348064d3 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -10,21 +10,19 @@ from bond_async import Action, BPUPSubscriptions, DeviceType import voluptuous as vol from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import BondConfigEntry from .const import ( ATTR_POWER_STATE, - DOMAIN, SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE, SERVICE_SET_LIGHT_POWER_TRACKED_STATE, ) from .entity import BondEntity -from .models import BondData from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) @@ -42,11 +40,11 @@ ENTITY_SERVICES = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BondConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond light devices.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index aa39f871c95..b8aaa81cd05 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -9,24 +9,23 @@ from bond_async import Action, DeviceType import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_POWER_STATE, DOMAIN, SERVICE_SET_POWER_TRACKED_STATE +from . import BondConfigEntry +from .const import ATTR_POWER_STATE, SERVICE_SET_POWER_TRACKED_STATE from .entity import BondEntity -from .models import BondData async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BondConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond generic devices.""" - data: BondData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data hub = data.hub bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index 087b3148ea7..61cf6d4e0ce 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -9,13 +9,15 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import DOMAIN # noqa: F401 from .coordinator import CO2SignalCoordinator PLATFORMS = [Platform.SENSOR] +CO2SignalConfigEntry = ConfigEntry[CO2SignalCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: CO2SignalConfigEntry) -> bool: """Set up CO2 Signal from a config entry.""" session = async_get_clientsession(hass) coordinator = CO2SignalCoordinator( @@ -23,11 +25,11 @@ 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: CO2SignalConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/co2signal/diagnostics.py b/homeassistant/components/co2signal/diagnostics.py index 4e553f0c7da..a071950440f 100644 --- a/homeassistant/components/co2signal/diagnostics.py +++ b/homeassistant/components/co2signal/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_API_KEY from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import CO2SignalCoordinator +from . import CO2SignalConfigEntry TO_REDACT = {CONF_API_KEY} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: CO2SignalConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: CO2SignalCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 5b11fd85827..1b964edf591 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -12,13 +12,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import CO2SignalConfigEntry from .const import ATTRIBUTION, DOMAIN from .coordinator import CO2SignalCoordinator @@ -53,10 +53,12 @@ SENSORS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: CO2SignalConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the CO2signal sensor.""" - coordinator: CO2SignalCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( [CO2Sensor(coordinator, description) for description in SENSORS], False ) 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/ecobee/const.py b/homeassistant/components/ecobee/const.py index 0eed0ab67f9..8adc7f9638b 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -49,6 +49,7 @@ PLATFORMS = [ Platform.NOTIFY, Platform.NUMBER, Platform.SENSOR, + Platform.SWITCH, Platform.WEATHER, ] diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 7e461230600..b11bdf8afb0 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -10,7 +10,7 @@ }, "iot_class": "cloud_polling", "loggers": ["pyecobee"], - "requirements": ["python-ecobee-api==0.2.17"], + "requirements": ["python-ecobee-api==0.2.18"], "zeroconf": [ { "type": "_ecobee._tcp.local." 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/ecobee/switch.py b/homeassistant/components/ecobee/switch.py new file mode 100644 index 00000000000..44528a5f421 --- /dev/null +++ b/homeassistant/components/ecobee/switch.py @@ -0,0 +1,90 @@ +"""Support for using switch with ecobee thermostats.""" + +from __future__ import annotations + +import logging +from typing import Any + +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 homeassistant.util import dt as dt_util + +from . import EcobeeData +from .const import DOMAIN +from .entity import EcobeeBaseEntity + +_LOGGER = logging.getLogger(__name__) + +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ecobee thermostat switch entity.""" + data: EcobeeData = hass.data[DOMAIN] + + async_add_entities( + ( + EcobeeVentilator20MinSwitch(data, index) + for index, thermostat in enumerate(data.ecobee.thermostats) + if thermostat["settings"]["ventilatorType"] != "none" + ), + True, + ) + + +class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity): + """A Switch class, representing 20 min timer for an ecobee thermostat with ventilator attached.""" + + _attr_has_entity_name = True + _attr_name = "Ventilator 20m Timer" + + def __init__( + self, + data: EcobeeData, + thermostat_index: int, + ) -> None: + """Initialize ecobee ventilator platform.""" + super().__init__(data, thermostat_index) + self._attr_unique_id = f"{self.base_unique_id}_ventilator_20m_timer" + self._attr_is_on = False + self.update_without_throttle = False + self._operating_timezone = dt_util.get_time_zone( + self.thermostat["location"]["timeZone"] + ) + + async def async_update(self) -> None: + """Get the latest state from the thermostat.""" + + if self.update_without_throttle: + await self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + await self.data.update() + + ventilator_off_date_time = self.thermostat["settings"]["ventilatorOffDateTime"] + + self._attr_is_on = ventilator_off_date_time and dt_util.parse_datetime( + ventilator_off_date_time, raise_on_error=True + ).replace(tzinfo=self._operating_timezone) >= dt_util.now( + self._operating_timezone + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Set ventilator 20 min timer on.""" + await self.hass.async_add_executor_job( + self.data.ecobee.set_ventilator_timer, self.thermostat_index, True + ) + self.update_without_throttle = True + + async def async_turn_off(self, **kwargs: Any) -> None: + """Set ventilator 20 min timer off.""" + await self.hass.async_add_executor_job( + self.data.ecobee.set_ventilator_timer, self.thermostat_index, False + ) + self.update_without_throttle = True diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index d0c34b0cf9a..f29c8177dfd 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.0"] + "requirements": ["env-canada==0.6.2"] } diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 1928bb15bc2..c4d1c02ee74 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections.abc import Callable from datetime import datetime, timedelta import logging from typing import Final, TypeVar @@ -42,28 +41,24 @@ PLATFORMS: Final = [Platform.SENSOR] _FroniusCoordinatorT = TypeVar("_FroniusCoordinatorT", bound=FroniusCoordinatorBase) +FroniusConfigEntry = ConfigEntry["FroniusSolarNet"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool: """Set up fronius from a config entry.""" host = entry.data[CONF_HOST] fronius = Fronius(async_get_clientsession(hass), host) solar_net = FroniusSolarNet(hass, entry, fronius) await solar_net.init_devices() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net + entry.runtime_data = solar_net 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: FroniusConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - solar_net = hass.data[DOMAIN].pop(entry.entry_id) - while solar_net.cleanup_callbacks: - solar_net.cleanup_callbacks.pop()() - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_remove_config_entry_device( @@ -81,7 +76,6 @@ class FroniusSolarNet: ) -> None: """Initialize FroniusSolarNet class.""" self.hass = hass - self.cleanup_callbacks: list[Callable[[], None]] = [] self.config_entry = entry self.coordinator_lock = asyncio.Lock() self.fronius = fronius @@ -151,7 +145,7 @@ class FroniusSolarNet: ) # Setup periodic re-scan - self.cleanup_callbacks.append( + self.config_entry.async_on_unload( async_track_time_interval( self.hass, self._init_devices_inverter, diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 1ecd74a6e09..71ecb4e762e 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -121,7 +121,7 @@ class FroniusCoordinatorBase( async_add_entities(new_entities) _add_entities_for_unregistered_descriptors() - self.solar_net.cleanup_callbacks.append( + self.solar_net.config_entry.async_on_unload( self.async_add_listener(_add_entities_for_unregistered_descriptors) ) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 2d79086d8ba..3b283c33326 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, POWER_VOLT_AMPERE_REACTIVE, @@ -44,7 +43,7 @@ from .const import ( ) if TYPE_CHECKING: - from . import FroniusSolarNet + from . import FroniusConfigEntry from .coordinator import ( FroniusCoordinatorBase, FroniusInverterUpdateCoordinator, @@ -60,11 +59,11 @@ ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: FroniusConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Fronius sensor entities based on a config entry.""" - solar_net: FroniusSolarNet = hass.data[DOMAIN][config_entry.entry_id] + solar_net = config_entry.runtime_data for inverter_coordinator in solar_net.inverter_coordinators: inverter_coordinator.add_entities_for_seen_keys( diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index a8fd9027984..489226742ae 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Collection, Mapping import logging from typing import Any -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from .const import ATTR_AUTO, ATTR_ORDER, DOMAIN, GROUP_ORDER, REG_KEY -from .registry import GroupIntegrationRegistry +from .registry import GroupIntegrationRegistry, SingleStateType ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -133,6 +133,7 @@ class Group(Entity): _attr_should_poll = False tracking: tuple[str, ...] trackable: tuple[str, ...] + single_state_type_key: SingleStateType | None def __init__( self, @@ -153,7 +154,7 @@ class Group(Entity): self._attr_name = name self._state: str | None = None self._attr_icon = icon - self._set_tracked(entity_ids) + self._entity_ids = entity_ids self._on_off: dict[str, bool] = {} self._assumed: dict[str, bool] = {} self._on_states: set[str] = set() @@ -287,6 +288,7 @@ class Group(Entity): if not entity_ids: self.tracking = () self.trackable = () + self.single_state_type_key = None return registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] @@ -294,16 +296,42 @@ class Group(Entity): tracking: list[str] = [] trackable: list[str] = [] + single_state_type_set: set[SingleStateType] = set() for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] tracking.append(ent_id_lower) if domain not in excluded_domains: trackable.append(ent_id_lower) + if domain in registry.state_group_mapping: + single_state_type_set.add(registry.state_group_mapping[domain]) + elif domain == DOMAIN: + # If a group contains another group we check if that group + # has a specific single state type + if ent_id in registry.state_group_mapping: + single_state_type_set.add(registry.state_group_mapping[ent_id]) + else: + single_state_type_set.add(SingleStateType(STATE_ON, STATE_OFF)) + + if len(single_state_type_set) == 1: + self.single_state_type_key = next(iter(single_state_type_set)) + # To support groups with nested groups we store the state type + # per group entity_id if there is a single state type + registry.state_group_mapping[self.entity_id] = self.single_state_type_key + else: + self.single_state_type_key = None + self.async_on_remove(self._async_deregister) self.trackable = tuple(trackable) self.tracking = tuple(tracking) + @callback + def _async_deregister(self) -> None: + """Deregister group entity from the registry.""" + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + if self.entity_id in registry.state_group_mapping: + registry.state_group_mapping.pop(self.entity_id) + @callback def _async_start(self, _: HomeAssistant | None = None) -> None: """Start tracking members and write state.""" @@ -342,6 +370,7 @@ class Group(Entity): async def async_added_to_hass(self) -> None: """Handle addition to Home Assistant.""" + self._set_tracked(self._entity_ids) self.async_on_remove(start.async_at_start(self.hass, self._async_start)) async def async_will_remove_from_hass(self) -> None: @@ -430,12 +459,14 @@ class Group(Entity): # have the same on state we use this state # and its hass.data[REG_KEY].on_off_mapping to off if num_on_states == 1: - on_state = list(self._on_states)[0] + on_state = next(iter(self._on_states)) # If we do not have an on state for any domains # we use None (which will be STATE_UNKNOWN) elif num_on_states == 0: self._state = None return + if self.single_state_type_key: + on_state = self.single_state_type_key.on_state # If the entity domains have more than one # on state, we use STATE_ON/STATE_OFF else: @@ -443,9 +474,10 @@ class Group(Entity): group_is_on = self.mode(self._on_off.values()) if group_is_on: self._state = on_state + elif self.single_state_type_key: + self._state = self.single_state_type_key.off_state else: - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - self._state = registry.on_off_mapping[on_state] + self._state = STATE_OFF def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 9ddf7c0b409..4ce89a4c725 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -1,8 +1,12 @@ -"""Provide the functionality to group entities.""" +"""Provide the functionality to group entities. + +Legacy group support will not be extended for new domains. +""" from __future__ import annotations -from typing import TYPE_CHECKING, Protocol +from dataclasses import dataclass +from typing import Protocol from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback @@ -12,9 +16,6 @@ from homeassistant.helpers.integration_platform import ( from .const import DOMAIN, REG_KEY -if TYPE_CHECKING: - from .entity import Group - async def async_setup(hass: HomeAssistant) -> None: """Set up the Group integration registry of integration platforms.""" @@ -43,6 +44,14 @@ def _process_group_platform( platform.async_describe_on_off_states(hass, registry) +@dataclass(frozen=True, slots=True) +class SingleStateType: + """Dataclass to store a single state type.""" + + on_state: str + off_state: str + + class GroupIntegrationRegistry: """Class to hold a registry of integrations.""" @@ -53,8 +62,7 @@ class GroupIntegrationRegistry: self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} self.on_states_by_domain: dict[str, set[str]] = {} self.exclude_domains: set[str] = set() - self.state_group_mapping: dict[str, tuple[str, str]] = {} - self.group_entities: set[Group] = set() + self.state_group_mapping: dict[str, SingleStateType] = {} @callback def exclude_domain(self, domain: str) -> None: @@ -65,12 +73,16 @@ class GroupIntegrationRegistry: def on_off_states( self, domain: str, on_states: set[str], default_on_state: str, off_state: str ) -> None: - """Register on and off states for the current domain.""" + """Register on and off states for the current domain. + + Legacy group support will not be extended for new domains. + """ for on_state in on_states: if on_state not in self.on_off_mapping: self.on_off_mapping[on_state] = off_state - if len(on_states) == 1 and off_state not in self.off_on_mapping: + if off_state not in self.off_on_mapping: self.off_on_mapping[off_state] = default_on_state + self.state_group_mapping[domain] = SingleStateType(default_on_state, off_state) self.on_states_by_domain[domain] = on_states diff --git a/homeassistant/components/imgw_pib/__init__.py b/homeassistant/components/imgw_pib/__init__.py index f3dd66eb23d..54511e76020 100644 --- a/homeassistant/components/imgw_pib/__init__.py +++ b/homeassistant/components/imgw_pib/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_STATION_ID from .coordinator import ImgwPibDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/imgw_pib/binary_sensor.py b/homeassistant/components/imgw_pib/binary_sensor.py new file mode 100644 index 00000000000..1c4cc738f8f --- /dev/null +++ b/homeassistant/components/imgw_pib/binary_sensor.py @@ -0,0 +1,82 @@ +"""IMGW-PIB binary sensor platform.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from imgw_pib.model import HydrologicalData + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ImgwPibConfigEntry +from .coordinator import ImgwPibDataUpdateCoordinator +from .entity import ImgwPibEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class ImgwPibBinarySensorEntityDescription(BinarySensorEntityDescription): + """IMGW-PIB sensor entity description.""" + + value: Callable[[HydrologicalData], bool | None] + + +BINARY_SENSOR_TYPES: tuple[ImgwPibBinarySensorEntityDescription, ...] = ( + ImgwPibBinarySensorEntityDescription( + key="flood_warning", + translation_key="flood_warning", + device_class=BinarySensorDeviceClass.SAFETY, + value=lambda data: data.flood_warning, + ), + ImgwPibBinarySensorEntityDescription( + key="flood_alarm", + translation_key="flood_alarm", + device_class=BinarySensorDeviceClass.SAFETY, + value=lambda data: data.flood_alarm, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ImgwPibConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add a IMGW-PIB binary sensor entity from a config_entry.""" + coordinator = entry.runtime_data.coordinator + + async_add_entities( + ImgwPibBinarySensorEntity(coordinator, description) + for description in BINARY_SENSOR_TYPES + if getattr(coordinator.data, description.key) is not None + ) + + +class ImgwPibBinarySensorEntity(ImgwPibEntity, BinarySensorEntity): + """Define IMGW-PIB binary sensor entity.""" + + entity_description: ImgwPibBinarySensorEntityDescription + + def __init__( + self, + coordinator: ImgwPibDataUpdateCoordinator, + description: ImgwPibBinarySensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{coordinator.station_id}_{description.key}" + self.entity_description = description + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/imgw_pib/entity.py b/homeassistant/components/imgw_pib/entity.py new file mode 100644 index 00000000000..ef55c0e9a4e --- /dev/null +++ b/homeassistant/components/imgw_pib/entity.py @@ -0,0 +1,22 @@ +"""Define the IMGW-PIB entity.""" + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION +from .coordinator import ImgwPibDataUpdateCoordinator + + +class ImgwPibEntity(CoordinatorEntity[ImgwPibDataUpdateCoordinator]): + """Define IMGW-PIB entity.""" + + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ImgwPibDataUpdateCoordinator, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/imgw_pib/icons.json b/homeassistant/components/imgw_pib/icons.json index 29aa19a4b56..7ad72efca80 100644 --- a/homeassistant/components/imgw_pib/icons.json +++ b/homeassistant/components/imgw_pib/icons.json @@ -1,5 +1,19 @@ { "entity": { + "binary_sensor": { + "flood_warning": { + "default": "mdi:check-circle", + "state": { + "on": "mdi:home-flood" + } + }, + "flood_alarm": { + "default": "mdi:check-circle", + "state": { + "on": "mdi:home-flood" + } + } + }, "sensor": { "water_level": { "default": "mdi:waves" diff --git a/homeassistant/components/imgw_pib/sensor.py b/homeassistant/components/imgw_pib/sensor.py index 1df651faa52..d3f2162c056 100644 --- a/homeassistant/components/imgw_pib/sensor.py +++ b/homeassistant/components/imgw_pib/sensor.py @@ -17,11 +17,10 @@ from homeassistant.const import UnitOfLength, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ImgwPibConfigEntry -from .const import ATTRIBUTION from .coordinator import ImgwPibDataUpdateCoordinator +from .entity import ImgwPibEntity PARALLEL_UPDATES = 1 @@ -70,13 +69,9 @@ async def async_setup_entry( ) -class ImgwPibSensorEntity( - CoordinatorEntity[ImgwPibDataUpdateCoordinator], SensorEntity -): +class ImgwPibSensorEntity(ImgwPibEntity, SensorEntity): """Define IMGW-PIB sensor entity.""" - _attr_attribution = ATTRIBUTION - _attr_has_entity_name = True entity_description: ImgwPibSensorEntityDescription def __init__( @@ -88,7 +83,6 @@ class ImgwPibSensorEntity( super().__init__(coordinator) self._attr_unique_id = f"{coordinator.station_id}_{description.key}" - self._attr_device_info = coordinator.device_info self.entity_description = description @property diff --git a/homeassistant/components/imgw_pib/strings.json b/homeassistant/components/imgw_pib/strings.json index 9a17dcf7087..b4246861d4c 100644 --- a/homeassistant/components/imgw_pib/strings.json +++ b/homeassistant/components/imgw_pib/strings.json @@ -17,6 +17,14 @@ } }, "entity": { + "binary_sensor": { + "flood_alarm": { + "name": "Flood alarm" + }, + "flood_warning": { + "name": "Flood warning" + } + }, "sensor": { "water_level": { "name": "Water level" 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/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cc1ae3ddce1..3178d68c9d6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,16 +13,7 @@ import voluptuous as vol from homeassistant import config as conf_util from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PAYLOAD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, - SERVICE_RELOAD, -) +from homeassistant.const import CONF_DISCOVERY, CONF_PAYLOAD, SERVICE_RELOAD from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ( ConfigValidationError, @@ -122,45 +113,6 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" -CONFIG_ENTRY_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_ID, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, - CONF_DISCOVERY, - CONF_DISCOVERY_PREFIX, - CONF_KEEPALIVE, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_TLS_INSECURE, - CONF_TRANSPORT, - CONF_WS_PATH, - CONF_WS_HEADERS, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - -REMOVED_OPTIONS = vol.All( - cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 - cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 - cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4 - cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3 - cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4 - cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4 - cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4 - cv.removed(CONF_PORT), # Removed in HA Core 2023.4 - cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4 - cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4 - cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4 - cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4 -) - # We accept 2 schemes for configuring manual MQTT items # # Preferred style: @@ -187,7 +139,6 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.All( cv.ensure_list, cv.remove_falsy, - [REMOVED_OPTIONS], [CONFIG_SCHEMA_BASE], ) }, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5fadf6ba590..c3efe5667ad 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -86,7 +86,6 @@ ABBREVIATIONS = { "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", - "lrst_t": "last_reset_topic", "lrst_val_tpl": "last_reset_value_template", "max": "max", "min": "min", diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 1a7dfbbc507..c848c2955fb 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -197,7 +197,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): entry: ConfigEntry | None _hassio_discovery: dict[str, Any] | None = None - _reauth_config_entry: ConfigEntry | None = None @staticmethod @callback 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/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 9ba6308e07c..5457011d122 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -58,7 +58,6 @@ from .models import ( _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = "expire_after" -CONF_LAST_RESET_TOPIC = "last_reset_topic" CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" @@ -101,17 +100,11 @@ def validate_sensor_state_class_config(config: ConfigType) -> ConfigType: PLATFORM_SCHEMA_MODERN = vol.All( - # Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840 - # Removed in HA Core 2023.6.0 - cv.removed(CONF_LAST_RESET_TOPIC), _PLATFORM_SCHEMA_BASE, validate_sensor_state_class_config, ) DISCOVERY_SCHEMA = vol.All( - # Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840 - # Removed in HA Core 2023.6.0 - cv.removed(CONF_LAST_RESET_TOPIC), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_sensor_state_class_config, ) 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/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 5dccca54772..aa541c470f1 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers import config_entry_oauth2_flow from . import api, config_flow from .const import DOMAIN +from .coordinator import OndiloIcoCoordinator from .oauth_impl import OndiloOauth2Implementation PLATFORMS = [Platform.SENSOR] @@ -26,8 +27,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) + coordinator = OndiloIcoCoordinator( + hass, api.OndiloClient(hass, entry, implementation) + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ondilo_ico/coordinator.py b/homeassistant/components/ondilo_ico/coordinator.py new file mode 100644 index 00000000000..d3e9b4a4e11 --- /dev/null +++ b/homeassistant/components/ondilo_ico/coordinator.py @@ -0,0 +1,37 @@ +"""Define an object to coordinate fetching Ondilo ICO data.""" + +from datetime import timedelta +import logging +from typing import Any + +from ondilo import OndiloError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from . import DOMAIN +from .api import OndiloClient + +_LOGGER = logging.getLogger(__name__) + + +class OndiloIcoCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): + """Class to manage fetching Ondilo ICO data from API.""" + + def __init__(self, hass: HomeAssistant, api: OndiloClient) -> None: + """Initialize.""" + super().__init__( + hass, + logger=_LOGGER, + name=DOMAIN, + update_interval=timedelta(minutes=5), + ) + self.api = api + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Fetch data from API endpoint.""" + try: + return await self.hass.async_add_executor_job(self.api.get_all_pools_data) + + except OndiloError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 17569fd784f..5f21fb6a909 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -2,12 +2,6 @@ from __future__ import annotations -from datetime import timedelta -import logging -from typing import Any - -from ondilo import OndiloError - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -24,14 +18,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .api import OndiloClient from .const import DOMAIN +from .coordinator import OndiloIcoCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -78,66 +68,30 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) -SCAN_INTERVAL = timedelta(minutes=5) -_LOGGER = logging.getLogger(__name__) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Ondilo ICO sensors.""" - api: OndiloClient = hass.data[DOMAIN][entry.entry_id] + coordinator: OndiloIcoCoordinator = hass.data[DOMAIN][entry.entry_id] - async def async_update_data() -> list[dict[str, Any]]: - """Fetch data from API endpoint. - - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - try: - return await hass.async_add_executor_job(api.get_all_pools_data) - - except OndiloError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="sensor", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=SCAN_INTERVAL, + async_add_entities( + OndiloICO(coordinator, poolidx, description) + for poolidx, pool in enumerate(coordinator.data) + for sensor in pool["sensors"] + for description in SENSOR_TYPES + if description.key == sensor["data_type"] ) - # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() - entities = [] - for poolidx, pool in enumerate(coordinator.data): - entities.extend( - [ - OndiloICO(coordinator, poolidx, description) - for sensor in pool["sensors"] - for description in SENSOR_TYPES - if description.key == sensor["data_type"] - ] - ) - - async_add_entities(entities) - - -class OndiloICO( - CoordinatorEntity[DataUpdateCoordinator[list[dict[str, Any]]]], SensorEntity -): +class OndiloICO(CoordinatorEntity[OndiloIcoCoordinator], SensorEntity): """Representation of a Sensor.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator[list[dict[str, Any]]], + coordinator: OndiloIcoCoordinator, poolidx: int, description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index ffbfc1799c5..2a91f1b1b38 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations import openai import voluptuous as vol -from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import ( @@ -115,5 +114,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data[DOMAIN].pop(entry.entry_id) - conversation.async_unset_agent(hass, entry) return True diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index ee4a107c241..f992849f9b1 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -3,7 +3,7 @@ import logging DOMAIN = "openai_conversation" -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__package__) CONF_PROMPT = "prompt" DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 158b155c75d..3c94d66ee4a 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -44,6 +44,8 @@ class OpenAIConversationEntity( ): """OpenAI conversation agent.""" + _attr_has_entity_name = True + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the agent.""" self.hass = hass diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 2aef39ce59b..afe1e781a88 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -69,7 +69,9 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): try: self.cached_map = self._create_image(starting_map) except HomeAssistantError: - # If we failed to update the image on init, we set cached_map to empty bytes so that we are unavailable and can try again later. + # If we failed to update the image on init, + # we set cached_map to empty bytes + # so that we are unavailable and can try again later. self.cached_map = b"" self._attr_entity_category = EntityCategory.DIAGNOSTIC @@ -84,7 +86,11 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): return self.map_flag == self.coordinator.current_map def is_map_valid(self) -> bool: - """Update this map if it is the current active map, and the vacuum is cleaning or if it has never been set at all.""" + """Update the map if it is valid. + + Update this map if it is the currently active map, and the + vacuum is cleaning, or if it has never been set at all. + """ return self.cached_map == b"" or ( self.is_selected and self.image_last_updated is not None @@ -134,8 +140,9 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def create_coordinator_maps( coord: RoborockDataUpdateCoordinator, ) -> list[RoborockMap]: - """Get the starting map information for all maps for this device. The following steps must be done synchronously. + """Get the starting map information for all maps for this device. + The following steps must be done synchronously. Only one map can be loaded at a time per device. """ entities = [] @@ -161,7 +168,8 @@ async def create_coordinator_maps( map_update = await asyncio.gather( *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True ) - # If we fail to get the map -> We should set it to empty byte, still create it, and set it as unavailable. + # If we fail to get the map, we should set it to empty byte, + # still create it, and set it as unavailable. api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b"" entities.append( RoborockMap( diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index 5a097572d98..a8bcc335991 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/serial", "iot_class": "local_polling", - "requirements": ["pyserial-asyncio==0.6"] + "requirements": ["pyserial-asyncio-fast==0.11"] } diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 5f2b1ea3c3c..9d60877bd1b 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -7,7 +7,7 @@ import json import logging from serial import SerialException -import serial_asyncio +import serial_asyncio_fast as serial_asyncio import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 982d654c8fe..504c2f505a7 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==76"], + "requirements": ["aiounifi==77"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 08badae8cd0..483ab89b02e 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -57,4 +57,6 @@ SKU_TO_BASE_DEVICE = { "Vital100S": "Vital100S", "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-WEU": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-WUK": "Vital100S", # Alt ID Model Vital100S } diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 4fc08cf983d..ce7c9105781 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==1.0.0"] + "requirements": ["pywaze==1.0.1"] } diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index b1511b2f5bb..9a0ca62542e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,9 +21,8 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.3", + "bellows==0.38.4", "pyserial==3.5", - "pyserial-asyncio==0.6", "zha-quirks==0.0.115", "zigpy-deconz==0.23.1", "zigpy==0.64.0", 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/package_constraints.txt b/homeassistant/package_constraints.txt index fec28850240..b743897e871 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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.0 +habluetooth==2.8.1 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 @@ -192,3 +192,8 @@ pycountry>=23.12.11 # scapy<2.5.0 will not work with python3.12 scapy>=2.5.0 + +# tuf isn't updated to deal with breaking changes in securesystemslib==1.0. +# Only tuf>=4 includes a constraint to <1.0. +# https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 +tuf>=4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3639b1581ff..623c0667147 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==76 +aiounifi==77 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -413,7 +413,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.7.1 +airthings-ble==0.8.0 # homeassistant.components.airthings airthings-cloud==0.2.0 @@ -541,7 +541,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.3 +bellows==0.38.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 @@ -804,7 +804,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.0 +env-canada==0.6.2 # homeassistant.components.season ephem==4.1.5 @@ -1035,7 +1035,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.8.0 +habluetooth==2.8.1 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -2122,12 +2122,9 @@ pyschlage==2024.2.0 # homeassistant.components.sensibo pysensibo==1.0.36 -# homeassistant.components.zha -pyserial-asyncio-fast==0.11 - # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.6 +pyserial-asyncio-fast==0.11 # homeassistant.components.acer_projector # homeassistant.components.crownstone @@ -2218,7 +2215,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.17 +python-ecobee-api==0.2.18 # homeassistant.components.etherscan python-etherscan-api==0.0.3 @@ -2376,7 +2373,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.0 +pywaze==1.0.1 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54a6bc4a6ff..77c24137b96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==76 +aiounifi==77 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -386,7 +386,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.7.1 +airthings-ble==0.8.0 # homeassistant.components.airthings airthings-cloud==0.2.0 @@ -466,7 +466,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.3 +bellows==0.38.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 @@ -658,7 +658,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.0 +env-canada==0.6.2 # homeassistant.components.season ephem==4.1.5 @@ -849,7 +849,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.8.0 +habluetooth==2.8.1 # homeassistant.components.cloud hass-nabucasa==0.78.0 @@ -1661,12 +1661,9 @@ pyschlage==2024.2.0 # homeassistant.components.sensibo pysensibo==1.0.36 -# homeassistant.components.zha -pyserial-asyncio-fast==0.11 - # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.6 +pyserial-asyncio-fast==0.11 # homeassistant.components.acer_projector # homeassistant.components.crownstone @@ -1733,7 +1730,7 @@ python-awair==0.2.4 python-bsblan==0.5.18 # homeassistant.components.ecobee -python-ecobee-api==0.2.17 +python-ecobee-api==0.2.18 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.12 @@ -1849,7 +1846,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.0 +pywaze==1.0.1 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 602a9fe934b..1f2f4bcab66 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -214,6 +214,11 @@ pycountry>=23.12.11 # scapy<2.5.0 will not work with python3.12 scapy>=2.5.0 + +# tuf isn't updated to deal with breaking changes in securesystemslib==1.0. +# Only tuf>=4 includes a constraint to <1.0. +# https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 +tuf>=4.0.0 """ GENERATED_MESSAGE = ( diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index c90eb432c25..164264634b8 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -81,7 +81,7 @@ async def setup_airvisual_pro_fixture(hass, config, pro): return_value=pro, ), patch("homeassistant.components.airvisual_pro.NodeSamba", return_value=pro), - patch("homeassistant.components.airvisual.PLATFORMS", []), + patch("homeassistant.components.airvisual_pro.PLATFORMS", []), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index bc034d0e6f3..05161ba32cd 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -2,7 +2,7 @@ from syrupy import SnapshotAssertion -from homeassistant.components.ambient_station import DOMAIN +from homeassistant.components.ambient_station import AmbientStationConfigEntry from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,14 +11,14 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, - config_entry, + config_entry: AmbientStationConfigEntry, hass_client: ClientSessionGenerator, data_station, setup_config_entry, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - ambient = hass.data[DOMAIN][config_entry.entry_id] + ambient = config_entry.runtime_data ambient.stations = data_station assert ( await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8c26745d541..ebc50779c9c 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import DEFAULT_ADDRESS -from habluetooth import scanner +from habluetooth import scanner, set_manager from habluetooth.wrappers import HaBleakScannerWrapper import pytest @@ -1154,6 +1154,7 @@ async def test_async_discovered_device_api( ) -> None: """Test the async_discovered_device API.""" mock_bt = [] + set_manager(None) with ( patch( "homeassistant.components.bluetooth.async_get_bluetooth", @@ -1169,8 +1170,10 @@ async def test_async_discovered_device_api( }, ), ): - assert not bluetooth.async_discovered_service_info(hass) - assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + with pytest.raises(RuntimeError, match="BluetoothManager has not been set"): + assert not bluetooth.async_discovered_service_info(hass) + with pytest.raises(RuntimeError, match="BluetoothManager has not been set"): + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): @@ -2744,6 +2747,7 @@ async def test_async_ble_device_from_address( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None ) -> None: """Test the async_ble_device_from_address api.""" + set_manager(None) mock_bt = [] with ( patch( @@ -2760,11 +2764,15 @@ async def test_async_ble_device_from_address( }, ), ): - assert not bluetooth.async_discovered_service_info(hass) - assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert ( - bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None - ) + with pytest.raises(RuntimeError, match="BluetoothManager has not been set"): + assert not bluetooth.async_discovered_service_info(hass) + with pytest.raises(RuntimeError, match="BluetoothManager has not been set"): + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + with pytest.raises(RuntimeError, match="BluetoothManager has not been set"): + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") + is None + ) await async_setup_with_default_adapter(hass) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 3ad589d2d10..0aaff0edfe7 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -6,7 +6,7 @@ from aiohttp import ClientConnectionError, ClientResponseError from bond_async import DeviceType import pytest -from homeassistant.components.bond.const import DOMAIN +from homeassistant.components.bond import DOMAIN, BondData from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST @@ -107,7 +107,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains( assert result is True await hass.async_block_till_done() - assert config_entry.entry_id in hass.data[DOMAIN] + assert isinstance(config_entry.runtime_data, BondData) assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "ZXXX12345" @@ -148,7 +148,6 @@ async def test_unload_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.entry_id not in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.NOT_LOADED @@ -194,7 +193,6 @@ async def test_old_identifiers_are_removed( assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "ZXXX12345" @@ -238,7 +236,6 @@ async def test_smart_by_bond_device_suggested_area( assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "KXXX12345" @@ -287,7 +284,6 @@ async def test_bridge_device_suggested_area( assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "ZXXX12345" 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/ecobee/__init__.py b/tests/components/ecobee/__init__.py index 52c6fcc6a4e..f89729df9bb 100644 --- a/tests/components/ecobee/__init__.py +++ b/tests/components/ecobee/__init__.py @@ -65,6 +65,9 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = { "identifier": 8675309, "name": "ecobee", "modelNumber": "athenaSmart", + "utcTime": "2022-01-01 10:00:00", + "thermostatTime": "2022-01-01 6:00:00", + "location": {"timeZone": "America/Toronto"}, "program": { "climates": [ {"name": "Climate1", "climateRef": "c1"}, @@ -92,7 +95,8 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = { "humidifierMode": "manual", "humidity": "30", "hasHeatPump": True, - "ventilatorType": "none", + "ventilatorType": "hrv", + "ventilatorOffDateTime": "2022-01-01 6:00:00", }, "equipmentStatus": "fan", "events": [ diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index d8621bd8c4b..c86782d9c0b 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -4,6 +4,11 @@ "identifier": 8675309, "name": "ecobee", "modelNumber": "athenaSmart", + "utcTime": "2022-01-01 10:00:00", + "thermostatTime": "2022-01-01 6:00:00", + "location": { + "timeZone": "America/Toronto" + }, "program": { "climates": [ { "name": "Climate1", "climateRef": "c1" }, @@ -30,6 +35,7 @@ "ventilatorType": "hrv", "ventilatorMinOnTimeHome": 20, "ventilatorMinOnTimeAway": 10, + "ventilatorOffDateTime": "2022-01-01 6:00:00", "isVentilatorTimerOn": false, "hasHumidifier": true, "humidifierMode": "manual", diff --git a/tests/components/ecobee/test_switch.py b/tests/components/ecobee/test_switch.py new file mode 100644 index 00000000000..383abf9644c --- /dev/null +++ b/tests/components/ecobee/test_switch.py @@ -0,0 +1,115 @@ +"""The test for the ecobee thermostat switch module.""" + +import copy +from datetime import datetime, timedelta +from unittest import mock +from unittest.mock import patch + +import pytest + +from homeassistant.components.ecobee.switch import DATE_FORMAT +from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .common import setup_platform + +from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP + +VENTILATOR_20MIN_ID = "switch.ecobee_ventilator_20m_timer" +THERMOSTAT_ID = 0 + + +@pytest.fixture(name="data") +def data_fixture(): + """Set up data mock.""" + data = mock.Mock() + data.return_value = copy.deepcopy(GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP) + return data + + +async def test_ventilator_20min_attributes(hass: HomeAssistant) -> None: + """Test the ventilator switch on home attributes are correct.""" + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + +async def test_ventilator_20min_when_on(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = ( + datetime.now() + timedelta(days=1) + ).strftime(DATE_FORMAT) + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "on" + + data.reset_mock() + + +async def test_ventilator_20min_when_off(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = ( + datetime.now() - timedelta(days=1) + ).strftime(DATE_FORMAT) + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + data.reset_mock() + + +async def test_ventilator_20min_when_empty(hass: HomeAssistant, data) -> None: + """Test the ventilator switch goes on.""" + + data.return_value["settings"]["ventilatorOffDateTime"] = "" + with mock.patch("pyecobee.Ecobee.get_thermostat", data): + await setup_platform(hass, DOMAIN) + + state = hass.states.get(VENTILATOR_20MIN_ID) + assert state.state == "off" + + data.reset_mock() + + +async def test_turn_on_20min_ventilator(hass: HomeAssistant) -> None: + """Test the switch 20 min timer (On).""" + + with patch( + "homeassistant.components.ecobee.Ecobee.set_ventilator_timer" + ) as mock_set_20min_ventilator: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: VENTILATOR_20MIN_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, True) + + +async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None: + """Test the switch 20 min timer (off).""" + + with patch( + "homeassistant.components.ecobee.Ecobee.set_ventilator_timer" + ) as mock_set_20min_ventilator: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: VENTILATOR_20MIN_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index d3f2747933e..9dbd1fe1f6e 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -10,6 +10,7 @@ from unittest.mock import patch import pytest from homeassistant.components import group +from homeassistant.components.group.registry import GroupIntegrationRegistry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, @@ -33,7 +34,116 @@ from homeassistant.setup import async_setup_component from . import common -from tests.common import MockConfigEntry, assert_setup_component +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + assert_setup_component, + mock_integration, + mock_platform, +) + + +async def help_test_mixed_entity_platforms_on_off_state_test( + hass: HomeAssistant, + on_off_states1: tuple[set[str], str, str], + on_off_states2: tuple[set[str], str, str], + entity_and_state1_state_2: tuple[str, str | None, str | None], + group_state1: str, + group_state2: str, + grouped_groups: bool = False, +) -> None: + """Help test on_off_states on mixed entity platforms.""" + + class MockGroupPlatform1(MockPlatform): + """Mock a group platform module for test1 integration.""" + + def async_describe_on_off_states( + self, hass: HomeAssistant, registry: GroupIntegrationRegistry + ) -> None: + """Describe group on off states.""" + registry.on_off_states("test1", *on_off_states1) + + class MockGroupPlatform2(MockPlatform): + """Mock a group platform module for test2 integration.""" + + def async_describe_on_off_states( + self, hass: HomeAssistant, registry: GroupIntegrationRegistry + ) -> None: + """Describe group on off states.""" + registry.on_off_states("test2", *on_off_states2) + + mock_integration(hass, MockModule(domain="test1")) + mock_platform(hass, "test1.group", MockGroupPlatform1()) + assert await async_setup_component(hass, "test1", {"test1": {}}) + + mock_integration(hass, MockModule(domain="test2")) + mock_platform(hass, "test2.group", MockGroupPlatform2()) + assert await async_setup_component(hass, "test2", {"test2": {}}) + + if grouped_groups: + assert await async_setup_component( + hass, + "group", + { + "group": { + "test1": { + "entities": [ + item[0] + for item in entity_and_state1_state_2 + if item[0].startswith("test1.") + ] + }, + "test2": { + "entities": [ + item[0] + for item in entity_and_state1_state_2 + if item[0].startswith("test2.") + ] + }, + "test": {"entities": ["group.test1", "group.test2"]}, + } + }, + ) + else: + assert await async_setup_component( + hass, + "group", + { + "group": { + "test": { + "entities": [item[0] for item in entity_and_state1_state_2] + }, + } + }, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("group.test") + assert state is not None + + # Set first state + for entity_id, state1, _ in entity_and_state1_state_2: + hass.states.async_set(entity_id, state1) + + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("group.test") + assert state is not None + assert state.state == group_state1 + + # Set second state + for entity_id, _, state2 in entity_and_state1_state_2: + hass.states.async_set(entity_id, state2) + + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("group.test") + assert state is not None + assert state.state == group_state2 async def test_setup_group_with_mixed_groupable_states(hass: HomeAssistant) -> None: @@ -1560,6 +1670,7 @@ async def test_group_that_references_a_group_of_covers(hass: HomeAssistant) -> N for entity_id in entity_ids: hass.states.async_set(entity_id, "closed") await hass.async_block_till_done() + assert await async_setup_component(hass, "cover", {}) assert await async_setup_component( hass, @@ -1643,6 +1754,7 @@ async def test_group_that_references_two_types_of_groups(hass: HomeAssistant) -> hass.states.async_set(entity_id, "home") await hass.async_block_till_done() + assert await async_setup_component(hass, "cover", {}) assert await async_setup_component(hass, "device_tracker", {}) assert await async_setup_component( hass, @@ -1884,3 +1996,216 @@ async def test_unhide_members_on_remove( # Check the group members are unhidden assert entity_registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert entity_registry.async_get(f"{group_type}.three").hidden_by == hidden_by + + +@pytest.mark.parametrize("grouped_groups", [False, True]) +@pytest.mark.parametrize( + ("on_off_states1", "on_off_states2"), + [ + ( + ( + { + "on_beer", + "on_milk", + }, + "on_beer", # default ON state test1 + "off_water", # default OFF state test1 + ), + ( + { + "on_beer", + "on_milk", + }, + "on_milk", # default ON state test2 + "off_wine", # default OFF state test2 + ), + ), + ], +) +@pytest.mark.parametrize( + ("entity_and_state1_state_2", "group_state1", "group_state2"), + [ + # All OFF states, no change, so group stays OFF + ( + [ + ("test1.ent1", "off_water", "off_water"), + ("test1.ent2", "off_water", "off_water"), + ("test2.ent1", "off_wine", "off_wine"), + ("test2.ent2", "off_wine", "off_wine"), + ], + STATE_OFF, + STATE_OFF, + ), + # All entities have state on_milk, but the state groups + # are different so the group status defaults to ON / OFF + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_milk"), + ("test2.ent1", "off_wine", "on_milk"), + ("test2.ent2", "off_wine", "on_milk"), + ], + STATE_OFF, + STATE_ON, + ), + # Only test1 entities in group, all at ON state + # group returns the default ON state `on_beer` + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_beer"), + ], + "off_water", + "on_beer", + ), + # Only test1 entities in group, all at ON state + # group returns the default ON state `on_beer` + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_milk"), + ], + "off_water", + "on_beer", + ), + # Only test2 entities in group, all at ON state + # group returns the default ON state `on_milk` + ( + [ + ("test2.ent1", "off_wine", "on_milk"), + ("test2.ent2", "off_wine", "on_milk"), + ], + "off_wine", + "on_milk", + ), + ], +) +async def test_entity_platforms_with_multiple_on_states_no_state_match( + hass: HomeAssistant, + on_off_states1: tuple[set[str], str, str], + on_off_states2: tuple[set[str], str, str], + entity_and_state1_state_2: tuple[str, str | None, str | None], + group_state1: str, + group_state2: str, + grouped_groups: bool, +) -> None: + """Test custom entity platforms with multiple ON states without state match. + + The test group 1 an 2 non matching (default_state_on, state_off) pairs. + """ + await help_test_mixed_entity_platforms_on_off_state_test( + hass, + on_off_states1, + on_off_states2, + entity_and_state1_state_2, + group_state1, + group_state2, + grouped_groups, + ) + + +@pytest.mark.parametrize("grouped_groups", [False, True]) +@pytest.mark.parametrize( + ("on_off_states1", "on_off_states2"), + [ + ( + ( + { + "on_beer", + "on_milk", + }, + "on_beer", # default ON state test1 + "off_water", # default OFF state test1 + ), + ( + { + "on_beer", + "on_wine", + }, + "on_beer", # default ON state test2 + "off_water", # default OFF state test2 + ), + ), + ], +) +@pytest.mark.parametrize( + ("entity_and_state1_state_2", "group_state1", "group_state2"), + [ + # All OFF states, no change, so group stays OFF + ( + [ + ("test1.ent1", "off_water", "off_water"), + ("test1.ent2", "off_water", "off_water"), + ("test2.ent1", "off_water", "off_water"), + ("test2.ent2", "off_water", "off_water"), + ], + "off_water", + "off_water", + ), + # All entities have ON state `on_milk` + # but the group state will default to on_beer + # which is the default ON state for both integrations. + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_milk"), + ("test2.ent1", "off_water", "on_milk"), + ("test2.ent2", "off_water", "on_milk"), + ], + "off_water", + "on_beer", + ), + # Only test1 entities in group, all at ON state + # group returns the default ON state `on_beer` + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_beer"), + ], + "off_water", + "on_beer", + ), + # Only test1 entities in group, all at ON state + # group returns the default ON state `on_beer` + ( + [ + ("test1.ent1", "off_water", "on_milk"), + ("test1.ent2", "off_water", "on_milk"), + ], + "off_water", + "on_beer", + ), + # Only test2 entities in group, all at ON state + # group returns the default ON state `on_milk` + ( + [ + ("test2.ent1", "off_water", "on_wine"), + ("test2.ent2", "off_water", "on_wine"), + ], + "off_water", + "on_beer", + ), + ], +) +async def test_entity_platforms_with_multiple_on_states_with_state_match( + hass: HomeAssistant, + on_off_states1: tuple[set[str], str, str], + on_off_states2: tuple[set[str], str, str], + entity_and_state1_state_2: tuple[str, str | None, str | None], + group_state1: str, + group_state2: str, + grouped_groups: bool, +) -> None: + """Test custom entity platforms with multiple ON states with a state match. + + The integrations test1 and test2 have matching (default_state_on, state_off) pairs. + """ + await help_test_mixed_entity_platforms_on_off_state_test( + hass, + on_off_states1, + on_off_states2, + entity_and_state1_state_2, + group_state1, + group_state2, + grouped_groups, + ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 572593d642b..ff038b620eb 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -818,7 +818,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(dev_reg.devices) == 6 supervisor_mock_data = { diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 0ce6a190f55..075909dfd63 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -3,15 +3,24 @@ import pytest from homeassistant.components import history +from homeassistant.components.recorder import Recorder from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE -from homeassistant.setup import setup_component +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.typing import RecorderInstanceGenerator @pytest.fixture -def hass_history(hass_recorder): - """Home Assistant fixture with history.""" - hass = hass_recorder() +async def mock_recorder_before_hass( + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + +@pytest.fixture +async def hass_history(hass: HomeAssistant, recorder_mock: Recorder) -> None: + """Home Assistant fixture with history.""" config = history.CONFIG_SCHEMA( { history.DOMAIN: { @@ -26,6 +35,4 @@ def hass_history(hass_recorder): } } ) - assert setup_component(hass, history.DOMAIN, config) - - return hass + assert await async_setup_component(hass, history.DOMAIN, config) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index d0712b968bc..7806b7c9ef4 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -24,7 +24,6 @@ from tests.components.recorder.common import ( assert_multiple_states_equal_without_context_and_last_changed, assert_states_equal_without_context, async_wait_recording_done, - wait_recording_done, ) from tests.typing import ClientSessionGenerator @@ -39,25 +38,26 @@ def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: @pytest.mark.usefixtures("hass_history") -def test_setup() -> None: +async def test_setup() -> None: """Test setup method of history.""" # Verification occurs in the fixture -def test_get_significant_states(hass_history) -> None: +async def test_get_significant_states(hass: HomeAssistant, hass_history) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = get_significant_states(hass, zero, four, entity_ids=list(states)) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_minimal_response(hass_history) -> None: +async def test_get_significant_states_minimal_response( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned. When minimal responses is set only the first and @@ -67,8 +67,7 @@ def test_get_significant_states_minimal_response(hass_history) -> None: includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = get_significant_states( hass, zero, four, minimal_response=True, entity_ids=list(states) ) @@ -122,15 +121,16 @@ def test_get_significant_states_minimal_response(hass_history) -> None: ) -def test_get_significant_states_with_initial(hass_history) -> None: +async def test_get_significant_states_with_initial( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: if entity_id == "media_player.test": @@ -149,15 +149,16 @@ def test_get_significant_states_with_initial(hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_without_initial(hass_history) -> None: +async def test_get_significant_states_without_initial( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) one = zero + timedelta(seconds=1) one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) @@ -179,10 +180,11 @@ def test_get_significant_states_without_initial(hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_entity_id(hass_history) -> None: +async def test_get_significant_states_entity_id( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned for one entity.""" - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test"] @@ -193,10 +195,11 @@ def test_get_significant_states_entity_id(hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_multiple_entity_ids(hass_history) -> None: +async def test_get_significant_states_multiple_entity_ids( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned for one entity.""" - hass = hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test2"] @@ -211,14 +214,15 @@ def test_get_significant_states_multiple_entity_ids(hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_are_ordered(hass_history) -> None: +async def test_get_significant_states_are_ordered( + hass: HomeAssistant, hass_history +) -> None: """Test order of results from get_significant_states. When entity ids are given, the results should be returned with the data in the same order. """ - hass = hass_history - zero, four, _states = record_states(hass) + zero, four, _states = await async_record_states(hass) entity_ids = ["media_player.test", "media_player.test2"] hist = get_significant_states(hass, zero, four, entity_ids) assert list(hist.keys()) == entity_ids @@ -227,15 +231,14 @@ def test_get_significant_states_are_ordered(hass_history) -> None: assert list(hist.keys()) == entity_ids -def test_get_significant_states_only(hass_history) -> None: +async def test_get_significant_states_only(hass: HomeAssistant, hass_history) -> None: """Test significant states when significant_states_only is set.""" - hass = hass_history entity_id = "sensor.test" - def set_state(state, **kwargs): + async def set_state(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) start = dt_util.utcnow() - timedelta(minutes=4) @@ -243,19 +246,19 @@ def test_get_significant_states_only(hass_history) -> None: states = [] with freeze_time(start) as freezer: - set_state("123", attributes={"attribute": 10.64}) + await set_state("123", attributes={"attribute": 10.64}) freezer.move_to(points[0]) # Attributes are different, state not - states.append(set_state("123", attributes={"attribute": 21.42})) + states.append(await set_state("123", attributes={"attribute": 21.42})) freezer.move_to(points[1]) # state is different, attributes not - states.append(set_state("32", attributes={"attribute": 21.42})) + states.append(await set_state("32", attributes={"attribute": 21.42})) freezer.move_to(points[2]) # everything is different - states.append(set_state("412", attributes={"attribute": 54.23})) + states.append(await set_state("412", attributes={"attribute": 54.23})) hist = get_significant_states( hass, @@ -288,13 +291,13 @@ def test_get_significant_states_only(hass_history) -> None: ) -def check_significant_states(hass, zero, four, states, config): +async def check_significant_states(hass, zero, four, states, config): """Check if significant states are retrieved.""" hist = get_significant_states(hass, zero, four) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def record_states(hass): +async def async_record_states(hass): """Record some test states. We inject a bunch of state updates from media player, zone and @@ -308,10 +311,10 @@ def record_states(hass): zone = "zone.home" script_c = "script.can_cancel_this_one" - 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) zero = dt_util.utcnow() @@ -323,55 +326,63 @@ def record_states(hass): states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} with freeze_time(one) as freezer: states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + await set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) ) states[mp2].append( - set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + await set_state( + mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)} + ) ) states[mp3].append( - set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + await set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) ) states[therm].append( - set_state(therm, 20, attributes={"current_temperature": 19.5}) + await set_state(therm, 20, attributes={"current_temperature": 19.5}) ) freezer.move_to(one + timedelta(microseconds=1)) states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + await set_state( + mp, "YouTube", attributes={"media_title": str(sentinel.mt2)} + ) ) freezer.move_to(two) # This state will be skipped only different in time - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + await set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) # This state will be skipped because domain is excluded - set_state(zone, "zoning") + await set_state(zone, "zoning") states[script_c].append( - set_state(script_c, "off", attributes={"can_cancel": True}) + await set_state(script_c, "off", attributes={"can_cancel": True}) ) states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 19.8}) + await set_state(therm, 21, attributes={"current_temperature": 19.8}) ) states[therm2].append( - set_state(therm2, 20, attributes={"current_temperature": 19}) + await set_state(therm2, 20, attributes={"current_temperature": 19}) ) freezer.move_to(three) states[mp].append( - set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + await set_state( + mp, "Netflix", attributes={"media_title": str(sentinel.mt4)} + ) ) states[mp3].append( - set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + await set_state( + mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)} + ) ) # Attributes changed even though state is the same states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 20}) + await set_state(therm, 21, attributes={"current_temperature": 20}) ) return zero, four, states async def test_fetch_period_api( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history.""" await async_setup_component(hass, "history", {}) @@ -383,8 +394,8 @@ async def test_fetch_period_api( async def test_fetch_period_api_with_use_include_order( - recorder_mock: Recorder, hass: HomeAssistant, + recorder_mock: Recorder, hass_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -402,7 +413,7 @@ async def test_fetch_period_api_with_use_include_order( async def test_fetch_period_api_with_minimal_response( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with minimal_response.""" now = dt_util.utcnow() @@ -444,7 +455,7 @@ async def test_fetch_period_api_with_minimal_response( async def test_fetch_period_api_with_no_timestamp( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with no timestamp.""" await async_setup_component(hass, "history", {}) @@ -454,8 +465,8 @@ async def test_fetch_period_api_with_no_timestamp( async def test_fetch_period_api_with_include_order( - recorder_mock: Recorder, hass: HomeAssistant, + recorder_mock: Recorder, hass_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -482,7 +493,7 @@ async def test_fetch_period_api_with_include_order( async def test_entity_ids_limit_via_api( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test limiting history to entity_ids.""" await async_setup_component( @@ -508,7 +519,7 @@ async def test_entity_ids_limit_via_api( async def test_entity_ids_limit_via_api_with_skip_initial_state( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test limiting history to entity_ids with skip_initial_state.""" await async_setup_component( @@ -542,7 +553,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( async def test_fetch_period_api_before_history_started( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history for the far past.""" await async_setup_component( @@ -563,7 +574,7 @@ async def test_fetch_period_api_before_history_started( async def test_fetch_period_api_far_future( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history for the far future.""" await async_setup_component( @@ -584,7 +595,7 @@ async def test_fetch_period_api_far_future( async def test_fetch_period_api_with_invalid_datetime( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with an invalid date time.""" await async_setup_component( @@ -603,7 +614,7 @@ async def test_fetch_period_api_with_invalid_datetime( async def test_fetch_period_api_invalid_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with an invalid end time.""" await async_setup_component( @@ -625,7 +636,7 @@ async def test_fetch_period_api_invalid_end_time( async def test_entity_ids_limit_via_api_with_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test limiting history to entity_ids with end_time.""" await async_setup_component( @@ -671,7 +682,7 @@ async def test_entity_ids_limit_via_api_with_end_time( async def test_fetch_period_api_with_no_entity_ids( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with minimal_response.""" await async_setup_component(hass, "history", {}) @@ -724,13 +735,13 @@ async def test_fetch_period_api_with_no_entity_ids( ], ) async def test_history_with_invalid_entity_ids( + hass: HomeAssistant, + recorder_mock: Recorder, + hass_client: ClientSessionGenerator, filter_entity_id, status_code, response_contains1, response_contains2, - recorder_mock: Recorder, - hass: HomeAssistant, - hass_client: ClientSessionGenerator, ) -> None: """Test sending valid and invalid entity_ids to the API.""" await async_setup_component( diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 2e26256da90..1b867cea584 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -27,7 +27,6 @@ from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, old_db_schema, - wait_recording_done, ) from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -40,33 +39,34 @@ def db_schema_30(): @pytest.fixture -def legacy_hass_history(hass_history): +def legacy_hass_history(hass: HomeAssistant, hass_history): """Home Assistant fixture to use legacy history recording.""" - instance = recorder.get_instance(hass_history) + instance = recorder.get_instance(hass) with patch.object(instance.states_meta_manager, "active", False): - yield hass_history + yield @pytest.mark.usefixtures("legacy_hass_history") -def test_setup() -> None: +async def test_setup() -> None: """Test setup method of history.""" # Verification occurs in the fixture -def test_get_significant_states(legacy_hass_history) -> None: +async def test_get_significant_states(hass: HomeAssistant, legacy_hass_history) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = legacy_hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = get_significant_states(hass, zero, four, entity_ids=list(states)) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_minimal_response(legacy_hass_history) -> None: +async def test_get_significant_states_minimal_response( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test that only significant states are returned. When minimal responses is set only the first and @@ -76,8 +76,7 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None: includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = legacy_hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) hist = get_significant_states( hass, zero, four, minimal_response=True, entity_ids=list(states) ) @@ -132,15 +131,16 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None: ) -def test_get_significant_states_with_initial(legacy_hass_history) -> None: +async def test_get_significant_states_with_initial( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = legacy_hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) one = zero + timedelta(seconds=1) one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) @@ -162,15 +162,16 @@ def test_get_significant_states_with_initial(legacy_hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_without_initial(legacy_hass_history) -> None: +async def test_get_significant_states_without_initial( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test that only significant states are returned. We should get back every thermostat change that includes an attribute change, but only the state updates for media player (attribute changes are not significant and not returned). """ - hass = legacy_hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) one = zero + timedelta(seconds=1) one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) @@ -193,13 +194,13 @@ def test_get_significant_states_without_initial(legacy_hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_entity_id(hass_history) -> None: +async def test_get_significant_states_entity_id( + hass: HomeAssistant, hass_history +) -> None: """Test that only significant states are returned for one entity.""" - hass = hass_history - instance = recorder.get_instance(hass) with patch.object(instance.states_meta_manager, "active", False): - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test"] @@ -210,10 +211,11 @@ def test_get_significant_states_entity_id(hass_history) -> None: assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_multiple_entity_ids(legacy_hass_history) -> None: +async def test_get_significant_states_multiple_entity_ids( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test that only significant states are returned for one entity.""" - hass = legacy_hass_history - zero, four, states = record_states(hass) + zero, four, states = await async_record_states(hass) del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test2"] @@ -228,14 +230,15 @@ def test_get_significant_states_multiple_entity_ids(legacy_hass_history) -> None assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def test_get_significant_states_are_ordered(legacy_hass_history) -> None: +async def test_get_significant_states_are_ordered( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test order of results from get_significant_states. When entity ids are given, the results should be returned with the data in the same order. """ - hass = legacy_hass_history - zero, four, _states = record_states(hass) + zero, four, _states = await async_record_states(hass) entity_ids = ["media_player.test", "media_player.test2"] hist = get_significant_states(hass, zero, four, entity_ids) assert list(hist.keys()) == entity_ids @@ -244,15 +247,16 @@ def test_get_significant_states_are_ordered(legacy_hass_history) -> None: assert list(hist.keys()) == entity_ids -def test_get_significant_states_only(legacy_hass_history) -> None: +async def test_get_significant_states_only( + hass: HomeAssistant, legacy_hass_history +) -> None: """Test significant states when significant_states_only is set.""" - hass = legacy_hass_history entity_id = "sensor.test" - def set_state(state, **kwargs): + async def set_state(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) start = dt_util.utcnow() - timedelta(minutes=4) @@ -260,19 +264,19 @@ def test_get_significant_states_only(legacy_hass_history) -> None: states = [] with freeze_time(start) as freezer: - set_state("123", attributes={"attribute": 10.64}) + await set_state("123", attributes={"attribute": 10.64}) freezer.move_to(points[0]) # Attributes are different, state not - states.append(set_state("123", attributes={"attribute": 21.42})) + states.append(await set_state("123", attributes={"attribute": 21.42})) freezer.move_to(points[1]) # state is different, attributes not - states.append(set_state("32", attributes={"attribute": 21.42})) + states.append(await set_state("32", attributes={"attribute": 21.42})) freezer.move_to(points[2]) # everything is different - states.append(set_state("412", attributes={"attribute": 54.23})) + states.append(await set_state("412", attributes={"attribute": 54.23})) hist = get_significant_states( hass, @@ -311,7 +315,7 @@ def check_significant_states(hass, zero, four, states, config): assert_dict_of_states_equal_without_context_and_last_changed(states, hist) -def record_states(hass): +async def async_record_states(hass): """Record some test states. We inject a bunch of state updates from media player, zone and @@ -325,10 +329,10 @@ def record_states(hass): zone = "zone.home" script_c = "script.can_cancel_this_one" - def set_state(entity_id, state, **kwargs): + async def async_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) zero = dt_util.utcnow() @@ -340,55 +344,69 @@ def record_states(hass): states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} with freeze_time(one) as freezer: states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + await async_set_state( + mp, "idle", attributes={"media_title": str(sentinel.mt1)} + ) ) states[mp2].append( - set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + await async_set_state( + mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)} + ) ) states[mp3].append( - set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + await async_set_state( + mp3, "idle", attributes={"media_title": str(sentinel.mt1)} + ) ) states[therm].append( - set_state(therm, 20, attributes={"current_temperature": 19.5}) + await async_set_state(therm, 20, attributes={"current_temperature": 19.5}) ) freezer.move_to(one + timedelta(microseconds=1)) states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + await async_set_state( + mp, "YouTube", attributes={"media_title": str(sentinel.mt2)} + ) ) freezer.move_to(two) # This state will be skipped only different in time - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + await async_set_state( + mp, "YouTube", attributes={"media_title": str(sentinel.mt3)} + ) # This state will be skipped because domain is excluded - set_state(zone, "zoning") + await async_set_state(zone, "zoning") states[script_c].append( - set_state(script_c, "off", attributes={"can_cancel": True}) + await async_set_state(script_c, "off", attributes={"can_cancel": True}) ) states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 19.8}) + await async_set_state(therm, 21, attributes={"current_temperature": 19.8}) ) states[therm2].append( - set_state(therm2, 20, attributes={"current_temperature": 19}) + await async_set_state(therm2, 20, attributes={"current_temperature": 19}) ) freezer.move_to(three) states[mp].append( - set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + await async_set_state( + mp, "Netflix", attributes={"media_title": str(sentinel.mt4)} + ) ) states[mp3].append( - set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + await async_set_state( + mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)} + ) ) # Attributes changed even though state is the same states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 20}) + await async_set_state(therm, 21, attributes={"current_temperature": 20}) ) return zero, four, states async def test_fetch_period_api( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history.""" await async_setup_component(hass, "history", {}) @@ -402,7 +420,7 @@ async def test_fetch_period_api( async def test_fetch_period_api_with_minimal_response( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with minimal_response.""" now = dt_util.utcnow() @@ -445,7 +463,7 @@ async def test_fetch_period_api_with_minimal_response( async def test_fetch_period_api_with_no_timestamp( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history with no timestamp.""" await async_setup_component(hass, "history", {}) @@ -457,7 +475,7 @@ async def test_fetch_period_api_with_no_timestamp( async def test_fetch_period_api_with_include_order( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test the fetch period view for history.""" await async_setup_component( @@ -481,7 +499,7 @@ async def test_fetch_period_api_with_include_order( async def test_entity_ids_limit_via_api( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test limiting history to entity_ids.""" await async_setup_component( @@ -509,7 +527,7 @@ async def test_entity_ids_limit_via_api( async def test_entity_ids_limit_via_api_with_skip_initial_state( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator ) -> None: """Test limiting history to entity_ids with skip_initial_state.""" await async_setup_component( @@ -545,7 +563,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( async def test_history_during_period( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period.""" now = dt_util.utcnow() @@ -693,7 +711,7 @@ async def test_history_during_period( async def test_history_during_period_impossible_conditions( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period returns when condition cannot be true.""" await async_setup_component(hass, "history", {}) @@ -757,10 +775,10 @@ async def test_history_during_period_impossible_conditions( "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] ) async def test_history_during_period_significant_domain( - time_zone, - recorder_mock: Recorder, hass: HomeAssistant, + recorder_mock: Recorder, hass_ws_client: WebSocketGenerator, + time_zone, ) -> None: """Test history_during_period with climate domain.""" hass.config.set_time_zone(time_zone) @@ -941,7 +959,7 @@ async def test_history_during_period_significant_domain( async def test_history_during_period_bad_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period bad state time.""" await async_setup_component( @@ -966,7 +984,7 @@ async def test_history_during_period_bad_start_time( async def test_history_during_period_bad_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period bad end time.""" now = dt_util.utcnow() diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index 70e2eb9470a..8ff3c91a3fc 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -39,7 +39,7 @@ def test_setup() -> None: async def test_history_during_period( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period.""" now = dt_util.utcnow() @@ -173,7 +173,7 @@ async def test_history_during_period( async def test_history_during_period_impossible_conditions( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period returns when condition cannot be true.""" await async_setup_component(hass, "history", {}) @@ -235,10 +235,10 @@ async def test_history_during_period_impossible_conditions( "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] ) async def test_history_during_period_significant_domain( - time_zone, - recorder_mock: Recorder, hass: HomeAssistant, + recorder_mock: Recorder, hass_ws_client: WebSocketGenerator, + time_zone, ) -> None: """Test history_during_period with climate domain.""" hass.config.set_time_zone(time_zone) @@ -403,7 +403,7 @@ async def test_history_during_period_significant_domain( async def test_history_during_period_bad_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period bad state time.""" await async_setup_component( @@ -427,7 +427,7 @@ async def test_history_during_period_bad_start_time( async def test_history_during_period_bad_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period bad end time.""" now = dt_util.utcnow() @@ -454,7 +454,7 @@ async def test_history_during_period_bad_end_time( async def test_history_stream_historical_only( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream.""" now = dt_util.utcnow() @@ -525,7 +525,7 @@ async def test_history_stream_historical_only( async def test_history_stream_significant_domain_historical_only( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test the stream with climate domain with historical states only.""" now = dt_util.utcnow() @@ -726,7 +726,7 @@ async def test_history_stream_significant_domain_historical_only( async def test_history_stream_bad_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream bad state time.""" await async_setup_component( @@ -750,7 +750,7 @@ async def test_history_stream_bad_start_time( async def test_history_stream_end_time_before_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with an end_time before the start_time.""" end_time = dt_util.utcnow() - timedelta(seconds=2) @@ -778,7 +778,7 @@ async def test_history_stream_end_time_before_start_time( async def test_history_stream_bad_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream bad end time.""" now = dt_util.utcnow() @@ -805,7 +805,7 @@ async def test_history_stream_bad_end_time( async def test_history_stream_live_no_attributes_minimal_response( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data and no_attributes and minimal_response.""" now = dt_util.utcnow() @@ -882,7 +882,7 @@ async def test_history_stream_live_no_attributes_minimal_response( async def test_history_stream_live( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data.""" now = dt_util.utcnow() @@ -985,7 +985,7 @@ async def test_history_stream_live( async def test_history_stream_live_minimal_response( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data and minimal_response.""" now = dt_util.utcnow() @@ -1082,7 +1082,7 @@ async def test_history_stream_live_minimal_response( async def test_history_stream_live_no_attributes( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data and no_attributes.""" now = dt_util.utcnow() @@ -1163,7 +1163,7 @@ async def test_history_stream_live_no_attributes( async def test_history_stream_live_no_attributes_minimal_response_specific_entities( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data and no_attributes and minimal_response with specific entities.""" now = dt_util.utcnow() @@ -1241,7 +1241,7 @@ async def test_history_stream_live_no_attributes_minimal_response_specific_entit async def test_history_stream_live_with_future_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream with history and live data with future end time.""" now = dt_util.utcnow() @@ -1334,8 +1334,8 @@ async def test_history_stream_live_with_future_end_time( @pytest.mark.parametrize("include_start_time_state", [True, False]) async def test_history_stream_before_history_starts( - recorder_mock: Recorder, hass: HomeAssistant, + recorder_mock: Recorder, hass_ws_client: WebSocketGenerator, include_start_time_state, ) -> None: @@ -1385,7 +1385,7 @@ async def test_history_stream_before_history_starts( async def test_history_stream_for_entity_with_no_possible_changes( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream for future with no possible changes where end time is less than or equal to now.""" await async_setup_component( @@ -1436,7 +1436,7 @@ async def test_history_stream_for_entity_with_no_possible_changes( async def test_overflow_queue( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test overflowing the history stream queue.""" now = dt_util.utcnow() @@ -1513,7 +1513,7 @@ async def test_overflow_queue( async def test_history_during_period_for_invalid_entity_ids( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period for valid and invalid entity ids.""" now = dt_util.utcnow() @@ -1656,7 +1656,7 @@ async def test_history_during_period_for_invalid_entity_ids( async def test_history_stream_for_invalid_entity_ids( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream for invalid and valid entity ids.""" @@ -1824,7 +1824,7 @@ async def test_history_stream_for_invalid_entity_ids( async def test_history_stream_historical_only_with_start_time_state_past( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history stream.""" await async_setup_component( diff --git a/tests/components/history/test_websocket_api_schema_32.py b/tests/components/history/test_websocket_api_schema_32.py index 6ef6f7225c1..301de387c80 100644 --- a/tests/components/history/test_websocket_api_schema_32.py +++ b/tests/components/history/test_websocket_api_schema_32.py @@ -24,7 +24,7 @@ def db_schema_32(): async def test_history_during_period( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator ) -> None: """Test history_during_period.""" now = dt_util.utcnow() diff --git a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..f314a4be590 --- /dev/null +++ b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr @@ -0,0 +1,195 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood alarm', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_alarm', + 'unique_id': '123_flood_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'River Name (Station Name) Flood alarm', + }), + 'context': , + 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood warning', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_warning', + 'unique_id': '123_flood_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'River Name (Station Name) Flood warning', + }), + 'context': , + 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.station_name_flood_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood alarm', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_alarm', + 'unique_id': '123_flood_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'alarm_level': 630.0, + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'Station Name Flood alarm', + }), + 'context': , + 'entity_id': 'binary_sensor.station_name_flood_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.station_name_flood_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood warning', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_warning', + 'unique_id': '123_flood_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_warning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'Station Name Flood warning', + 'warning_level': 590.0, + }), + 'context': , + 'entity_id': 'binary_sensor.station_name_flood_warning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/imgw_pib/test_binary_sensor.py b/tests/components/imgw_pib/test_binary_sensor.py new file mode 100644 index 00000000000..185d4b18575 --- /dev/null +++ b/tests/components/imgw_pib/test_binary_sensor.py @@ -0,0 +1,65 @@ +"""Test the IMGW-PIB binary sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from imgw_pib import ApiError +from syrupy import SnapshotAssertion + +from homeassistant.components.imgw_pib.const import UPDATE_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +ENTITY_ID = "binary_sensor.river_name_station_name_flood_alarm" + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_imgw_pib_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the binary sensor.""" + with patch("homeassistant.components.imgw_pib.PLATFORMS", [Platform.BINARY_SENSOR]): + await init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_availability( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_imgw_pib_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure that we mark the entities unavailable correctly when service is offline.""" + await init_integration(hass, mock_config_entry) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "off" + + mock_imgw_pib_client.get_hydrological_data.side_effect = ApiError("API Error") + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_imgw_pib_client.get_hydrological_data.side_effect = None + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "off" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 422ec84c091..576ba3f94b2 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -15,6 +15,13 @@ from homeassistant import config_entries from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -230,8 +237,8 @@ async def test_user_v5_connection_works( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_PROTOCOL: "5", + CONF_PORT: 2345, + CONF_PROTOCOL: "5", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY @@ -468,7 +475,7 @@ async def test_option_flow( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) @@ -482,9 +489,9 @@ async def test_option_flow( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", }, ) assert result["type"] is FlowResultType.FORM @@ -516,9 +523,9 @@ async def test_option_flow( assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_BIRTH_MESSAGE: { @@ -565,7 +572,7 @@ async def test_bad_certificate( file_id = mock_process_uploaded_file.file_id test_input = { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, mqtt.CONF_CERTIFICATE: file_id[mqtt.CONF_CERTIFICATE], mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT], mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY], @@ -599,11 +606,11 @@ async def test_bad_certificate( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - mqtt.CONF_CLIENT_ID: "custom1234", + CONF_PORT: 1234, + CONF_CLIENT_ID: "custom1234", mqtt.CONF_KEEPALIVE: 60, mqtt.CONF_TLS_INSECURE: False, - mqtt.CONF_PROTOCOL: "3.1.1", + CONF_PROTOCOL: "3.1.1", }, ) await hass.async_block_till_done() @@ -618,13 +625,13 @@ async def test_bad_certificate( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, mqtt.CONF_KEEPALIVE: 60, "set_client_cert": set_client_cert, "set_ca_cert": set_ca_cert, mqtt.CONF_TLS_INSECURE: tls_insecure, - mqtt.CONF_PROTOCOL: "3.1.1", - mqtt.CONF_CLIENT_ID: "custom1234", + CONF_PROTOCOL: "3.1.1", + CONF_CLIENT_ID: "custom1234", }, ) test_input["set_client_cert"] = set_client_cert @@ -664,7 +671,7 @@ async def test_keepalive_validation( test_input = { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, mqtt.CONF_KEEPALIVE: input_value, } @@ -676,8 +683,8 @@ async def test_keepalive_validation( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - mqtt.CONF_CLIENT_ID: "custom1234", + CONF_PORT: 1234, + CONF_CLIENT_ID: "custom1234", }, ) @@ -715,7 +722,7 @@ async def test_disable_birth_will( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) await hass.async_block_till_done() @@ -731,9 +738,9 @@ async def test_disable_birth_will( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", }, ) assert result["type"] is FlowResultType.FORM @@ -763,9 +770,9 @@ async def test_disable_birth_will( assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_BIRTH_MESSAGE: {}, @@ -791,7 +798,7 @@ async def test_invalid_discovery_prefix( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", }, @@ -808,7 +815,7 @@ async def test_invalid_discovery_prefix( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, }, ) assert result["type"] is FlowResultType.FORM @@ -829,7 +836,7 @@ async def test_invalid_discovery_prefix( assert result["errors"]["base"] == "bad_discovery_prefix" assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", } @@ -873,9 +880,9 @@ async def test_option_flow_default_suggested_values( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 1234, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", mqtt.CONF_DISCOVERY: True, mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "ha_state/online", @@ -898,11 +905,11 @@ async def test_option_flow_default_suggested_values( assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, } suggested = { - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + CONF_USERNAME: "user", + CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -913,9 +920,9 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "us3r", - mqtt.CONF_PASSWORD: "p4ss", + CONF_PORT: 2345, + CONF_USERNAME: "us3r", + CONF_PASSWORD: "p4ss", }, ) assert result["type"] is FlowResultType.FORM @@ -960,11 +967,11 @@ async def test_option_flow_default_suggested_values( assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, } suggested = { - mqtt.CONF_USERNAME: "us3r", - mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + CONF_USERNAME: "us3r", + CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -973,7 +980,7 @@ async def test_option_flow_default_suggested_values( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, + user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" @@ -1030,7 +1037,7 @@ async def test_skipping_advanced_options( test_input = { mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, + CONF_PORT: 2345, "advanced_options": advanced_options, } @@ -1042,7 +1049,7 @@ async def test_skipping_advanced_options( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) @@ -1067,24 +1074,24 @@ async def test_skipping_advanced_options( ( { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_USERNAME: "username", - mqtt.CONF_PASSWORD: "verysecret", + CONF_USERNAME: "username", + CONF_PASSWORD: "verysecret", }, { - mqtt.CONF_USERNAME: "username", - mqtt.CONF_PASSWORD: "newpassword", + CONF_USERNAME: "username", + CONF_PASSWORD: "newpassword", }, "newpassword", ), ( { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_USERNAME: "username", - mqtt.CONF_PASSWORD: "verysecret", + CONF_USERNAME: "username", + CONF_PASSWORD: "verysecret", }, { - mqtt.CONF_USERNAME: "username", - mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + CONF_USERNAME: "username", + CONF_PASSWORD: PWD_NOT_CHANGED, }, "verysecret", ), @@ -1153,7 +1160,7 @@ async def test_step_reauth( assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - assert config_entry.data.get(mqtt.CONF_PASSWORD) == new_password + assert config_entry.data.get(CONF_PASSWORD) == new_password await hass.async_block_till_done() @@ -1167,7 +1174,7 @@ async def test_options_user_connection_fails( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -1176,7 +1183,7 @@ async def test_options_user_connection_fails( mock_try_connection_time_out.reset_mock() result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={mqtt.CONF_BROKER: "bad-broker", mqtt.CONF_PORT: 2345}, + user_input={mqtt.CONF_BROKER: "bad-broker", CONF_PORT: 2345}, ) assert result["type"] is FlowResultType.FORM @@ -1187,7 +1194,7 @@ async def test_options_user_connection_fails( # Check config entry did not update assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, } @@ -1201,7 +1208,7 @@ async def test_options_bad_birth_message_fails( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) @@ -1212,7 +1219,7 @@ async def test_options_bad_birth_message_fails( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, + user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, ) assert result["type"] is FlowResultType.FORM @@ -1228,7 +1235,7 @@ async def test_options_bad_birth_message_fails( # Check config entry did not update assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, } @@ -1242,7 +1249,7 @@ async def test_options_bad_will_message_fails( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) @@ -1253,7 +1260,7 @@ async def test_options_bad_will_message_fails( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, + user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, ) assert result["type"] is FlowResultType.FORM @@ -1269,7 +1276,7 @@ async def test_options_bad_will_message_fails( # Check config entry did not update assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, } @@ -1290,9 +1297,9 @@ async def test_try_connection_with_advanced_parameters( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + CONF_PORT: 1234, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_CERTIFICATE: "auto", mqtt.CONF_TLS_INSECURE: True, @@ -1323,15 +1330,15 @@ async def test_try_connection_with_advanced_parameters( assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, "set_client_cert": True, "set_ca_cert": "auto", } suggested = { - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + CONF_USERNAME: "user", + CONF_PASSWORD: PWD_NOT_CHANGED, mqtt.CONF_TLS_INSECURE: True, - mqtt.CONF_PROTOCOL: "3.1.1", + CONF_PROTOCOL: "3.1.1", mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_WS_PATH: "/path/", mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}', @@ -1348,9 +1355,9 @@ async def test_try_connection_with_advanced_parameters( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "us3r", - mqtt.CONF_PASSWORD: "p4ss", + CONF_PORT: 2345, + CONF_USERNAME: "us3r", + CONF_PASSWORD: "p4ss", "set_ca_cert": "auto", "set_client_cert": True, mqtt.CONF_TLS_INSECURE: True, @@ -1409,7 +1416,7 @@ async def test_setup_with_advanced_settings( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, }, ) @@ -1427,21 +1434,21 @@ async def test_setup_with_advanced_settings( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "secret", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "secret", "advanced_options": True, }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema - assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] + assert result["data_schema"].schema[CONF_CLIENT_ID] assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE] assert result["data_schema"].schema["set_client_cert"] assert result["data_schema"].schema["set_ca_cert"] assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE] - assert result["data_schema"].schema[mqtt.CONF_PROTOCOL] + assert result["data_schema"].schema[CONF_PROTOCOL] assert result["data_schema"].schema[mqtt.CONF_TRANSPORT] assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema @@ -1451,26 +1458,26 @@ async def test_setup_with_advanced_settings( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "secret", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "secret", mqtt.CONF_KEEPALIVE: 30, "set_ca_cert": "auto", "set_client_cert": True, mqtt.CONF_TLS_INSECURE: True, - mqtt.CONF_PROTOCOL: "3.1.1", + CONF_PROTOCOL: "3.1.1", mqtt.CONF_TRANSPORT: "websockets", }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema - assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] + assert result["data_schema"].schema[CONF_CLIENT_ID] assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE] assert result["data_schema"].schema["set_client_cert"] assert result["data_schema"].schema["set_ca_cert"] assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE] - assert result["data_schema"].schema[mqtt.CONF_PROTOCOL] + assert result["data_schema"].schema[CONF_PROTOCOL] assert result["data_schema"].schema[mqtt.CONF_CLIENT_CERT] assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY] assert result["data_schema"].schema[mqtt.CONF_TRANSPORT] @@ -1482,9 +1489,9 @@ async def test_setup_with_advanced_settings( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "secret", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "secret", mqtt.CONF_KEEPALIVE: 30, "set_ca_cert": "auto", "set_client_cert": True, @@ -1507,9 +1514,9 @@ async def test_setup_with_advanced_settings( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "secret", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "secret", mqtt.CONF_KEEPALIVE: 30, "set_ca_cert": "auto", "set_client_cert": True, @@ -1537,9 +1544,9 @@ async def test_setup_with_advanced_settings( # Check config entry result assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "secret", + CONF_PORT: 2345, + CONF_USERNAME: "user", + CONF_PASSWORD: "secret", mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_CLIENT_CERT: "## mock client certificate file ##", mqtt.CONF_CLIENT_KEY: "## mock key file ##", @@ -1569,7 +1576,7 @@ async def test_change_websockets_transport_to_tcp( config_entry, data={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, mqtt.CONF_WS_PATH: "/some_path", @@ -1590,7 +1597,7 @@ async def test_change_websockets_transport_to_tcp( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, mqtt.CONF_TRANSPORT: "tcp", mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}', mqtt.CONF_WS_PATH: "/some_path", @@ -1611,7 +1618,7 @@ async def test_change_websockets_transport_to_tcp( # Check config entry result assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, + CONF_PORT: 1234, mqtt.CONF_TRANSPORT: "tcp", mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index f14c1bd5fc4..349a0603e48 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -6,6 +6,7 @@ from unittest.mock import ANY import pytest from homeassistant.components import mqtt +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 @@ -143,8 +144,8 @@ async def test_entry_diagnostics( { mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_PASSWORD: "hunter2", - mqtt.CONF_USERNAME: "my_user", + CONF_PASSWORD: "hunter2", + CONF_USERNAME: "my_user", } ], ) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 38ce5df25d8..9560e93e01a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1338,22 +1338,6 @@ async def test_discovery_expansion_without_encoding_and_value_template_2( ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings - "CONF_BIRTH_MESSAGE", - "CONF_BROKER", - "CONF_CERTIFICATE", - "CONF_CLIENT_CERT", - "CONF_CLIENT_ID", - "CONF_CLIENT_KEY", - "CONF_DISCOVERY", - "CONF_DISCOVERY_ID", - "CONF_DISCOVERY_PREFIX", - "CONF_EMBEDDED", - "CONF_KEEPALIVE", - "CONF_TLS_INSECURE", - "CONF_TRANSPORT", - "CONF_WILL_MESSAGE", - "CONF_WS_PATH", - "CONF_WS_HEADERS", # Integration info "CONF_SUPPORT_URL", # Undocumented device configuration @@ -1373,6 +1357,14 @@ ABBREVIATIONS_WHITE_LIST = [ "CONF_WHITE_VALUE", ] +EXCLUDED_MODULES = { + "const.py", + "config.py", + "config_flow.py", + "device_trigger.py", + "trigger.py", +} + async def test_missing_discover_abbreviations( hass: HomeAssistant, @@ -1383,7 +1375,7 @@ async def test_missing_discover_abbreviations( missing = [] regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") for fil in Path(mqtt.__file__).parent.rglob("*.py"): - if fil.name == "trigger.py": + if fil.name in EXCLUDED_MODULES: continue with open(fil, encoding="utf-8") as file: matches = re.findall(regex, file.read()) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 6cfb37df29b..019f153c62a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -31,6 +31,7 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, + CONF_PROTOCOL, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, @@ -2221,21 +2222,21 @@ async def test_setup_manual_mqtt_with_invalid_config( ( { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_PROTOCOL: "3.1", + CONF_PROTOCOL: "3.1", }, 3, ), ( { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_PROTOCOL: "3.1.1", + CONF_PROTOCOL: "3.1.1", }, 4, ), ( { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_PROTOCOL: "5", + CONF_PROTOCOL: "5", }, 5, ), 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/recorder/test_init.py b/tests/components/recorder/test_init.py index f0609f82229..d9f0e7d296f 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -198,7 +198,7 @@ async def test_shutdown_closes_connections( hass.set_state(CoreState.not_running) - instance = get_instance(hass) + instance = recorder.get_instance(hass) await instance.async_db_ready await hass.async_block_till_done() pool = instance.engine.pool diff --git a/tests/conftest.py b/tests/conftest.py index 4852a41c061..031469848ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -526,6 +526,7 @@ async def hass( load_registries: bool, hass_storage: dict[str, Any], request: pytest.FixtureRequest, + mock_recorder_before_hass: None, ) -> AsyncGenerator[HomeAssistant, None]: """Create a test instance of Home Assistant.""" @@ -1577,6 +1578,15 @@ async def recorder_mock( return await async_setup_recorder_instance(hass, recorder_config) +@pytest.fixture +def mock_recorder_before_hass() -> None: + """Mock the recorder. + + Override or parametrize this fixture with a fixture that mocks the recorder, + in the tests that need to test the recorder. + """ + + @pytest.fixture(name="enable_bluetooth") async def mock_enable_bluetooth( hass: HomeAssistant,