Merge branch 'dev' into block_pyserial_asyncio

This commit is contained in:
J. Nick Koston 2024-05-02 16:37:09 -05:00 committed by GitHub
commit 264df97069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 571 additions and 263 deletions

View File

@ -10,16 +10,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR] 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.""" """Set up AfterShip from a config entry."""
hass.data.setdefault(DOMAIN, {})
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session) 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: except AfterShipException as err:
raise ConfigEntryNotReady from 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) 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -8,7 +8,6 @@ from typing import Any, Final
from pyaftership import AfterShip, AfterShipException from pyaftership import AfterShip, AfterShipException
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -18,6 +17,7 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import AfterShipConfigEntry
from .const import ( from .const import (
ADD_TRACKING_SERVICE_SCHEMA, ADD_TRACKING_SERVICE_SCHEMA,
ATTR_TRACKINGS, ATTR_TRACKINGS,
@ -41,11 +41,11 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AfterShipConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AfterShip sensor entities based on a config entry.""" """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) async_add_entities([AfterShipSensor(aftership, config_entry.title)], True)

View File

@ -15,14 +15,16 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN # noqa: F401
from .coordinator import AirNowDataUpdateCoordinator from .coordinator import AirNowDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR] 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.""" """Set up AirNow from a config entry."""
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE] 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() await coordinator.async_config_entry_first_refresh()
# Store Entity and Initialize Platforms # Store Entity and Initialize Platforms
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
# Listen for option changes # Listen for option changes
entry.async_on_unload(entry.add_update_listener(update_listener)) 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 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 a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
@ -14,8 +13,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AirNowDataUpdateCoordinator from . import AirNowConfigEntry
from .const import DOMAIN
ATTR_LATITUDE_CAP = "Latitude" ATTR_LATITUDE_CAP = "Latitude"
ATTR_LONGITUDE_CAP = "Longitude" ATTR_LONGITUDE_CAP = "Longitude"
@ -40,10 +38,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: AirNowConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: AirNowDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
return async_redact_data( return async_redact_data(
{ {

View File

@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TIME, ATTR_TIME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -26,7 +25,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import get_time_zone from homeassistant.util.dt import get_time_zone
from . import AirNowDataUpdateCoordinator from . import AirNowConfigEntry, AirNowDataUpdateCoordinator
from .const import ( from .const import (
ATTR_API_AQI, ATTR_API_AQI,
ATTR_API_AQI_DESCRIPTION, ATTR_API_AQI_DESCRIPTION,
@ -116,11 +115,11 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AirNowConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AirNow sensor entities based on a config entry.""" """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] entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES]

View File

@ -6,13 +6,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import AirQCoordinator from .coordinator import AirQCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR] 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.""" """Set up air-Q from a config entry."""
coordinator = AirQCoordinator(hass, 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 # Query the device for the first time and initialise coordinator.data
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
# Record the coordinator in a global store entry.runtime_data = coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirQCoordinator from . import AirQConfigEntry, AirQCoordinator
from .const import ( from .const import (
ACTIVITY_BECQUEREL_PER_CUBIC_METER, ACTIVITY_BECQUEREL_PER_CUBIC_METER,
CONCENTRATION_GRAMS_PER_CUBIC_METER, CONCENTRATION_GRAMS_PER_CUBIC_METER,
DOMAIN,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -400,12 +398,12 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigEntry, entry: AirQConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up sensor entities based on a config entry.""" """Set up sensor entities based on a config entry."""
coordinator = hass.data[DOMAIN][config.entry_id] coordinator = entry.runtime_data
entities: list[AirQSensor] = [] entities: list[AirQSensor] = []

View File

@ -22,11 +22,11 @@ SCAN_INTERVAL = timedelta(minutes=6)
AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]] 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.""" """Set up Airthings from a config entry."""
hass.data.setdefault(DOMAIN, {})
airthings = Airthings( airthings = Airthings(
entry.data[CONF_ID], entry.data[CONF_ID],
entry.data[CONF_SECRET], 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() 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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 a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@ -27,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsDataCoordinatorType from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
from .const import DOMAIN from .const import DOMAIN
SENSORS: dict[str, SensorEntityDescription] = { SENSORS: dict[str, SensorEntityDescription] = {
@ -102,12 +101,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AirthingsConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Airthings sensor.""" """Set up the Airthings sensor."""
coordinator: AirthingsDataCoordinatorType = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
entities = [ entities = [
AirthingsHeaterEnergySensor( AirthingsHeaterEnergySensor(
coordinator, coordinator,

View File

@ -13,8 +13,10 @@ from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE] 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.""" """Set up Airtouch 5 from a config entry."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
@ -30,22 +32,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady from t raise ConfigEntryNotReady from t
# Store an API object for your platforms to access # 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 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() await client.disconnect()
client.ac_status_callbacks.clear() client.ac_status_callbacks.clear()
client.connection_state_callbacks.clear() client.connection_state_callbacks.clear()
client.data_packet_callbacks.clear() client.data_packet_callbacks.clear()
client.zone_status_callbacks.clear() client.zone_status_callbacks.clear()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok

View File

@ -34,12 +34,12 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import Airtouch5ConfigEntry
from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO
from .entity import Airtouch5Entity from .entity import Airtouch5Entity
@ -92,11 +92,11 @@ FAN_MODE_TO_SET_AC_FAN_SPEED = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: Airtouch5ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Airtouch 5 Climate entities.""" """Set up the Airtouch 5 Climate entities."""
client: Airtouch5SimpleClient = hass.data[DOMAIN][config_entry.entry_id] client = config_entry.runtime_data
entities: list[ClimateEntity] = [] entities: list[ClimateEntity] = []

View File

@ -8,7 +8,7 @@ from collections.abc import Callable, Collection, Mapping
import logging import logging
from typing import Any 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 ( from homeassistant.core import (
CALLBACK_TYPE, CALLBACK_TYPE,
Event, Event,
@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import async_track_state_change_event
from .const import ATTR_AUTO, ATTR_ORDER, DOMAIN, GROUP_ORDER, REG_KEY 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 + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
@ -133,6 +133,7 @@ class Group(Entity):
_attr_should_poll = False _attr_should_poll = False
tracking: tuple[str, ...] tracking: tuple[str, ...]
trackable: tuple[str, ...] trackable: tuple[str, ...]
single_state_type_key: SingleStateType | None
def __init__( def __init__(
self, self,
@ -153,7 +154,7 @@ class Group(Entity):
self._attr_name = name self._attr_name = name
self._state: str | None = None self._state: str | None = None
self._attr_icon = icon self._attr_icon = icon
self._set_tracked(entity_ids) self._entity_ids = entity_ids
self._on_off: dict[str, bool] = {} self._on_off: dict[str, bool] = {}
self._assumed: dict[str, bool] = {} self._assumed: dict[str, bool] = {}
self._on_states: set[str] = set() self._on_states: set[str] = set()
@ -287,6 +288,7 @@ class Group(Entity):
if not entity_ids: if not entity_ids:
self.tracking = () self.tracking = ()
self.trackable = () self.trackable = ()
self.single_state_type_key = None
return return
registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] registry: GroupIntegrationRegistry = self.hass.data[REG_KEY]
@ -294,16 +296,42 @@ class Group(Entity):
tracking: list[str] = [] tracking: list[str] = []
trackable: list[str] = [] trackable: list[str] = []
single_state_type_set: set[SingleStateType] = set()
for ent_id in entity_ids: for ent_id in entity_ids:
ent_id_lower = ent_id.lower() ent_id_lower = ent_id.lower()
domain = split_entity_id(ent_id_lower)[0] domain = split_entity_id(ent_id_lower)[0]
tracking.append(ent_id_lower) tracking.append(ent_id_lower)
if domain not in excluded_domains: if domain not in excluded_domains:
trackable.append(ent_id_lower) 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.trackable = tuple(trackable)
self.tracking = tuple(tracking) 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 @callback
def _async_start(self, _: HomeAssistant | None = None) -> None: def _async_start(self, _: HomeAssistant | None = None) -> None:
"""Start tracking members and write state.""" """Start tracking members and write state."""
@ -342,6 +370,7 @@ class Group(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle addition to Home Assistant.""" """Handle addition to Home Assistant."""
self._set_tracked(self._entity_ids)
self.async_on_remove(start.async_at_start(self.hass, self._async_start)) self.async_on_remove(start.async_at_start(self.hass, self._async_start))
async def async_will_remove_from_hass(self) -> None: 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 # have the same on state we use this state
# and its hass.data[REG_KEY].on_off_mapping to off # and its hass.data[REG_KEY].on_off_mapping to off
if num_on_states == 1: 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 # If we do not have an on state for any domains
# we use None (which will be STATE_UNKNOWN) # we use None (which will be STATE_UNKNOWN)
elif num_on_states == 0: elif num_on_states == 0:
self._state = None self._state = None
return return
if self.single_state_type_key:
on_state = self.single_state_type_key.on_state
# If the entity domains have more than one # If the entity domains have more than one
# on state, we use STATE_ON/STATE_OFF # on state, we use STATE_ON/STATE_OFF
else: else:
@ -443,9 +474,10 @@ class Group(Entity):
group_is_on = self.mode(self._on_off.values()) group_is_on = self.mode(self._on_off.values())
if group_is_on: if group_is_on:
self._state = on_state self._state = on_state
elif self.single_state_type_key:
self._state = self.single_state_type_key.off_state
else: else:
registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] self._state = STATE_OFF
self._state = registry.on_off_mapping[on_state]
def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]:

View File

@ -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 __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.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -12,9 +16,6 @@ from homeassistant.helpers.integration_platform import (
from .const import DOMAIN, REG_KEY from .const import DOMAIN, REG_KEY
if TYPE_CHECKING:
from .entity import Group
async def async_setup(hass: HomeAssistant) -> None: async def async_setup(hass: HomeAssistant) -> None:
"""Set up the Group integration registry of integration platforms.""" """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) 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 GroupIntegrationRegistry:
"""Class to hold a registry of integrations.""" """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.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON}
self.on_states_by_domain: dict[str, set[str]] = {} self.on_states_by_domain: dict[str, set[str]] = {}
self.exclude_domains: set[str] = set() self.exclude_domains: set[str] = set()
self.state_group_mapping: dict[str, tuple[str, str]] = {} self.state_group_mapping: dict[str, SingleStateType] = {}
self.group_entities: set[Group] = set()
@callback @callback
def exclude_domain(self, domain: str) -> None: def exclude_domain(self, domain: str) -> None:
@ -65,12 +73,16 @@ class GroupIntegrationRegistry:
def on_off_states( def on_off_states(
self, domain: str, on_states: set[str], default_on_state: str, off_state: str self, domain: str, on_states: set[str], default_on_state: str, off_state: str
) -> None: ) -> 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: for on_state in on_states:
if on_state not in self.on_off_mapping: if on_state not in self.on_off_mapping:
self.on_off_mapping[on_state] = off_state 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.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 self.on_states_by_domain[domain] = on_states

View File

@ -13,16 +13,7 @@ import voluptuous as vol
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import CONF_DISCOVERY, CONF_PAYLOAD, SERVICE_RELOAD
CONF_CLIENT_ID,
CONF_DISCOVERY,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
SERVICE_RELOAD,
)
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ( from homeassistant.exceptions import (
ConfigValidationError, ConfigValidationError,
@ -122,45 +113,6 @@ CONNECTION_SUCCESS = "connection_success"
CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED = "connection_failed"
CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" 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 # We accept 2 schemes for configuring manual MQTT items
# #
# Preferred style: # Preferred style:
@ -187,7 +139,6 @@ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.All( DOMAIN: vol.All(
cv.ensure_list, cv.ensure_list,
cv.remove_falsy, cv.remove_falsy,
[REMOVED_OPTIONS],
[CONFIG_SCHEMA_BASE], [CONFIG_SCHEMA_BASE],
) )
}, },

View File

@ -197,7 +197,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
entry: ConfigEntry | None entry: ConfigEntry | None
_hassio_discovery: dict[str, Any] | None = None _hassio_discovery: dict[str, Any] | None = None
_reauth_config_entry: ConfigEntry | None = None
@staticmethod @staticmethod
@callback @callback

View File

@ -69,7 +69,9 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity):
try: try:
self.cached_map = self._create_image(starting_map) self.cached_map = self._create_image(starting_map)
except HomeAssistantError: 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.cached_map = b""
self._attr_entity_category = EntityCategory.DIAGNOSTIC self._attr_entity_category = EntityCategory.DIAGNOSTIC
@ -84,7 +86,11 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity):
return self.map_flag == self.coordinator.current_map return self.map_flag == self.coordinator.current_map
def is_map_valid(self) -> bool: 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 ( return self.cached_map == b"" or (
self.is_selected self.is_selected
and self.image_last_updated is not None and self.image_last_updated is not None
@ -134,8 +140,9 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity):
async def create_coordinator_maps( async def create_coordinator_maps(
coord: RoborockDataUpdateCoordinator, coord: RoborockDataUpdateCoordinator,
) -> list[RoborockMap]: ) -> 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. Only one map can be loaded at a time per device.
""" """
entities = [] entities = []
@ -161,7 +168,8 @@ async def create_coordinator_maps(
map_update = await asyncio.gather( map_update = await asyncio.gather(
*[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True *[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"" api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b""
entities.append( entities.append(
RoborockMap( RoborockMap(

View File

@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiounifi"], "loggers": ["aiounifi"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiounifi==76"], "requirements": ["aiounifi==77"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -57,4 +57,6 @@ SKU_TO_BASE_DEVICE = {
"Vital100S": "Vital100S", "Vital100S": "Vital100S",
"LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S
"LAP-V102S-AASR": "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
} }

View File

@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.5.6 aiotractive==0.5.6
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==76 aiounifi==77
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0

View File

@ -359,7 +359,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.5.6 aiotractive==0.5.6
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==76 aiounifi==77
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0

View File

@ -10,6 +10,7 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components import group from homeassistant.components import group
from homeassistant.components.group.registry import GroupIntegrationRegistry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
@ -33,7 +34,116 @@ from homeassistant.setup import async_setup_component
from . import common 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: 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: for entity_id in entity_ids:
hass.states.async_set(entity_id, "closed") hass.states.async_set(entity_id, "closed")
await hass.async_block_till_done() await hass.async_block_till_done()
assert await async_setup_component(hass, "cover", {})
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -1643,6 +1754,7 @@ async def test_group_that_references_two_types_of_groups(hass: HomeAssistant) ->
hass.states.async_set(entity_id, "home") hass.states.async_set(entity_id, "home")
await hass.async_block_till_done() 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, "device_tracker", {})
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -1884,3 +1996,216 @@ async def test_unhide_members_on_remove(
# Check the group members are unhidden # 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}.one").hidden_by == hidden_by
assert entity_registry.async_get(f"{group_type}.three").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,
)

View File

@ -818,7 +818,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None:
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) 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 assert len(dev_reg.devices) == 6
supervisor_mock_data = { supervisor_mock_data = {

View File

@ -15,6 +15,13 @@ from homeassistant import config_entries
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -230,8 +237,8 @@ async def test_user_v5_connection_works(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_PROTOCOL: "5", CONF_PROTOCOL: "5",
}, },
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
@ -468,7 +475,7 @@ async def test_option_flow(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
}, },
) )
@ -482,9 +489,9 @@ async def test_option_flow(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -516,9 +523,9 @@ async def test_option_flow(
assert result["data"] == {} assert result["data"] == {}
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
mqtt.CONF_BIRTH_MESSAGE: { mqtt.CONF_BIRTH_MESSAGE: {
@ -565,7 +572,7 @@ async def test_bad_certificate(
file_id = mock_process_uploaded_file.file_id file_id = mock_process_uploaded_file.file_id
test_input = { test_input = {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_CERTIFICATE: file_id[mqtt.CONF_CERTIFICATE], mqtt.CONF_CERTIFICATE: file_id[mqtt.CONF_CERTIFICATE],
mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT], mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT],
mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY], mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY],
@ -599,11 +606,11 @@ async def test_bad_certificate(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_CLIENT_ID: "custom1234", CONF_CLIENT_ID: "custom1234",
mqtt.CONF_KEEPALIVE: 60, mqtt.CONF_KEEPALIVE: 60,
mqtt.CONF_TLS_INSECURE: False, mqtt.CONF_TLS_INSECURE: False,
mqtt.CONF_PROTOCOL: "3.1.1", CONF_PROTOCOL: "3.1.1",
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -618,13 +625,13 @@ async def test_bad_certificate(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_KEEPALIVE: 60, mqtt.CONF_KEEPALIVE: 60,
"set_client_cert": set_client_cert, "set_client_cert": set_client_cert,
"set_ca_cert": set_ca_cert, "set_ca_cert": set_ca_cert,
mqtt.CONF_TLS_INSECURE: tls_insecure, mqtt.CONF_TLS_INSECURE: tls_insecure,
mqtt.CONF_PROTOCOL: "3.1.1", CONF_PROTOCOL: "3.1.1",
mqtt.CONF_CLIENT_ID: "custom1234", CONF_CLIENT_ID: "custom1234",
}, },
) )
test_input["set_client_cert"] = set_client_cert test_input["set_client_cert"] = set_client_cert
@ -664,7 +671,7 @@ async def test_keepalive_validation(
test_input = { test_input = {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_KEEPALIVE: input_value, mqtt.CONF_KEEPALIVE: input_value,
} }
@ -676,8 +683,8 @@ async def test_keepalive_validation(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_CLIENT_ID: "custom1234", CONF_CLIENT_ID: "custom1234",
}, },
) )
@ -715,7 +722,7 @@ async def test_disable_birth_will(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -731,9 +738,9 @@ async def test_disable_birth_will(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -763,9 +770,9 @@ async def test_disable_birth_will(
assert result["data"] == {} assert result["data"] == {}
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
mqtt.CONF_BIRTH_MESSAGE: {}, mqtt.CONF_BIRTH_MESSAGE: {},
@ -791,7 +798,7 @@ async def test_invalid_discovery_prefix(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
}, },
@ -808,7 +815,7 @@ async def test_invalid_discovery_prefix(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -829,7 +836,7 @@ async def test_invalid_discovery_prefix(
assert result["errors"]["base"] == "bad_discovery_prefix" assert result["errors"]["base"] == "bad_discovery_prefix"
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
} }
@ -873,9 +880,9 @@ async def test_option_flow_default_suggested_values(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: { mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online", mqtt.ATTR_TOPIC: "ha_state/online",
@ -898,11 +905,11 @@ async def test_option_flow_default_suggested_values(
assert result["step_id"] == "broker" assert result["step_id"] == "broker"
defaults = { defaults = {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
} }
suggested = { suggested = {
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, CONF_PASSWORD: PWD_NOT_CHANGED,
} }
for key, value in defaults.items(): for key, value in defaults.items():
assert get_default(result["data_schema"].schema, key) == value assert get_default(result["data_schema"].schema, key) == value
@ -913,9 +920,9 @@ async def test_option_flow_default_suggested_values(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "us3r", CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: "p4ss", CONF_PASSWORD: "p4ss",
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
@ -960,11 +967,11 @@ async def test_option_flow_default_suggested_values(
assert result["step_id"] == "broker" assert result["step_id"] == "broker"
defaults = { defaults = {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
} }
suggested = { suggested = {
mqtt.CONF_USERNAME: "us3r", CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, CONF_PASSWORD: PWD_NOT_CHANGED,
} }
for key, value in defaults.items(): for key, value in defaults.items():
assert get_default(result["data_schema"].schema, key) == value 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 = await hass.config_entries.options.async_configure(
result["flow_id"], 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["type"] is FlowResultType.FORM
assert result["step_id"] == "options" assert result["step_id"] == "options"
@ -1030,7 +1037,7 @@ async def test_skipping_advanced_options(
test_input = { test_input = {
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
"advanced_options": advanced_options, "advanced_options": advanced_options,
} }
@ -1042,7 +1049,7 @@ async def test_skipping_advanced_options(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", 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_BROKER: "test-broker",
mqtt.CONF_USERNAME: "username", CONF_USERNAME: "username",
mqtt.CONF_PASSWORD: "verysecret", CONF_PASSWORD: "verysecret",
}, },
{ {
mqtt.CONF_USERNAME: "username", CONF_USERNAME: "username",
mqtt.CONF_PASSWORD: "newpassword", CONF_PASSWORD: "newpassword",
}, },
"newpassword", "newpassword",
), ),
( (
{ {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_USERNAME: "username", CONF_USERNAME: "username",
mqtt.CONF_PASSWORD: "verysecret", CONF_PASSWORD: "verysecret",
}, },
{ {
mqtt.CONF_USERNAME: "username", CONF_USERNAME: "username",
mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, CONF_PASSWORD: PWD_NOT_CHANGED,
}, },
"verysecret", "verysecret",
), ),
@ -1153,7 +1160,7 @@ async def test_step_reauth(
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1 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() await hass.async_block_till_done()
@ -1167,7 +1174,7 @@ async def test_options_user_connection_fails(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
}, },
) )
result = await hass.config_entries.options.async_init(config_entry.entry_id) 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() mock_try_connection_time_out.reset_mock()
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], 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 assert result["type"] is FlowResultType.FORM
@ -1187,7 +1194,7 @@ async def test_options_user_connection_fails(
# Check config entry did not update # Check config entry did not update
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", 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, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", 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 = await hass.config_entries.options.async_configure(
result["flow_id"], 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["type"] is FlowResultType.FORM
@ -1228,7 +1235,7 @@ async def test_options_bad_birth_message_fails(
# Check config entry did not update # Check config entry did not update
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", 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, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", 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 = await hass.config_entries.options.async_configure(
result["flow_id"], 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["type"] is FlowResultType.FORM
@ -1269,7 +1276,7 @@ async def test_options_bad_will_message_fails(
# Check config entry did not update # Check config entry did not update
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", 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, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", CONF_PASSWORD: "pass",
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_CERTIFICATE: "auto", mqtt.CONF_CERTIFICATE: "auto",
mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_TLS_INSECURE: True,
@ -1323,15 +1330,15 @@ async def test_try_connection_with_advanced_parameters(
assert result["step_id"] == "broker" assert result["step_id"] == "broker"
defaults = { defaults = {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
"set_client_cert": True, "set_client_cert": True,
"set_ca_cert": "auto", "set_ca_cert": "auto",
} }
suggested = { suggested = {
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, CONF_PASSWORD: PWD_NOT_CHANGED,
mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1", CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/path/", mqtt.CONF_WS_PATH: "/path/",
mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}', mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}',
@ -1348,9 +1355,9 @@ async def test_try_connection_with_advanced_parameters(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "another-broker", mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "us3r", CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: "p4ss", CONF_PASSWORD: "p4ss",
"set_ca_cert": "auto", "set_ca_cert": "auto",
"set_client_cert": True, "set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_TLS_INSECURE: True,
@ -1409,7 +1416,7 @@ async def test_setup_with_advanced_settings(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", 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"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret", CONF_PASSWORD: "secret",
"advanced_options": True, "advanced_options": True,
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "broker" assert result["step_id"] == "broker"
assert "advanced_options" not in result["data_schema"].schema 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[mqtt.CONF_KEEPALIVE]
assert result["data_schema"].schema["set_client_cert"] assert result["data_schema"].schema["set_client_cert"]
assert result["data_schema"].schema["set_ca_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_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 result["data_schema"].schema[mqtt.CONF_TRANSPORT]
assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema
assert mqtt.CONF_CLIENT_KEY 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"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret", CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_KEEPALIVE: 30,
"set_ca_cert": "auto", "set_ca_cert": "auto",
"set_client_cert": True, "set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1", CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "broker" assert result["step_id"] == "broker"
assert "advanced_options" not in result["data_schema"].schema 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[mqtt.CONF_KEEPALIVE]
assert result["data_schema"].schema["set_client_cert"] assert result["data_schema"].schema["set_client_cert"]
assert result["data_schema"].schema["set_ca_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_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_CERT]
assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY] assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY]
assert result["data_schema"].schema[mqtt.CONF_TRANSPORT] assert result["data_schema"].schema[mqtt.CONF_TRANSPORT]
@ -1482,9 +1489,9 @@ async def test_setup_with_advanced_settings(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret", CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_KEEPALIVE: 30,
"set_ca_cert": "auto", "set_ca_cert": "auto",
"set_client_cert": True, "set_client_cert": True,
@ -1507,9 +1514,9 @@ async def test_setup_with_advanced_settings(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret", CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_KEEPALIVE: 30,
"set_ca_cert": "auto", "set_ca_cert": "auto",
"set_client_cert": True, "set_client_cert": True,
@ -1537,9 +1544,9 @@ async def test_setup_with_advanced_settings(
# Check config entry result # Check config entry result
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345, CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user", CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret", CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_KEEPALIVE: 30,
mqtt.CONF_CLIENT_CERT: "## mock client certificate file ##", mqtt.CONF_CLIENT_CERT: "## mock client certificate file ##",
mqtt.CONF_CLIENT_KEY: "## mock key file ##", mqtt.CONF_CLIENT_KEY: "## mock key file ##",
@ -1569,7 +1576,7 @@ async def test_change_websockets_transport_to_tcp(
config_entry, config_entry,
data={ data={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"},
mqtt.CONF_WS_PATH: "/some_path", mqtt.CONF_WS_PATH: "/some_path",
@ -1590,7 +1597,7 @@ async def test_change_websockets_transport_to_tcp(
result["flow_id"], result["flow_id"],
user_input={ user_input={
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_TRANSPORT: "tcp", mqtt.CONF_TRANSPORT: "tcp",
mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}', mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}',
mqtt.CONF_WS_PATH: "/some_path", mqtt.CONF_WS_PATH: "/some_path",
@ -1611,7 +1618,7 @@ async def test_change_websockets_transport_to_tcp(
# Check config entry result # Check config entry result
assert config_entry.data == { assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, CONF_PORT: 1234,
mqtt.CONF_TRANSPORT: "tcp", mqtt.CONF_TRANSPORT: "tcp",
mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test",

View File

@ -6,6 +6,7 @@ from unittest.mock import ANY
import pytest import pytest
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er 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_BROKER: "mock-broker",
mqtt.CONF_BIRTH_MESSAGE: {}, mqtt.CONF_BIRTH_MESSAGE: {},
mqtt.CONF_PASSWORD: "hunter2", CONF_PASSWORD: "hunter2",
mqtt.CONF_USERNAME: "my_user", CONF_USERNAME: "my_user",
} }
], ],
) )

View File

@ -1338,22 +1338,6 @@ async def test_discovery_expansion_without_encoding_and_value_template_2(
ABBREVIATIONS_WHITE_LIST = [ ABBREVIATIONS_WHITE_LIST = [
# MQTT client/server/trigger settings # 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 # Integration info
"CONF_SUPPORT_URL", "CONF_SUPPORT_URL",
# Undocumented device configuration # Undocumented device configuration
@ -1373,6 +1357,14 @@ ABBREVIATIONS_WHITE_LIST = [
"CONF_WHITE_VALUE", "CONF_WHITE_VALUE",
] ]
EXCLUDED_MODULES = {
"const.py",
"config.py",
"config_flow.py",
"device_trigger.py",
"trigger.py",
}
async def test_missing_discover_abbreviations( async def test_missing_discover_abbreviations(
hass: HomeAssistant, hass: HomeAssistant,
@ -1383,7 +1375,7 @@ async def test_missing_discover_abbreviations(
missing = [] missing = []
regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]")
for fil in Path(mqtt.__file__).parent.rglob("*.py"): for fil in Path(mqtt.__file__).parent.rglob("*.py"):
if fil.name == "trigger.py": if fil.name in EXCLUDED_MODULES:
continue continue
with open(fil, encoding="utf-8") as file: with open(fil, encoding="utf-8") as file:
matches = re.findall(regex, file.read()) matches = re.findall(regex, file.read())

View File

@ -31,6 +31,7 @@ from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import ( from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ASSUMED_STATE,
CONF_PROTOCOL,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD, SERVICE_RELOAD,
@ -2221,21 +2222,21 @@ async def test_setup_manual_mqtt_with_invalid_config(
( (
{ {
mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_PROTOCOL: "3.1", CONF_PROTOCOL: "3.1",
}, },
3, 3,
), ),
( (
{ {
mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_PROTOCOL: "3.1.1", CONF_PROTOCOL: "3.1.1",
}, },
4, 4,
), ),
( (
{ {
mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_PROTOCOL: "5", CONF_PROTOCOL: "5",
}, },
5, 5,
), ),