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.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)

View File

@ -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)

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.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:

View File

@ -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(
{

View File

@ -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]

View File

@ -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)

View File

@ -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] = []

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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] = []

View File

@ -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]:

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 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

View File

@ -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],
)
},

View File

@ -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

View File

@ -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(

View File

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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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,
)

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.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 = {

View File

@ -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",

View File

@ -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",
}
],
)

View File

@ -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())

View File

@ -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,
),