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/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/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/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/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/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/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/requirements_all.txt b/requirements_all.txt index 67be2a99246..74208602ae8 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a91fb1c9f71..2edbe9e0db1 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 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/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, ),