Mqtt support config_entry unload (#70149)

* squashed commits for rebase

* Flake

* Fix reloading issue manual legacy items

* Improve ACS sync for unsubscribe at disconnect

* Processed review comments

* Update homeassistant/components/mqtt/client.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* No need to await entry setup

* Remove complication is_connected

* Update homeassistant/components/mqtt/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis 2022-07-12 11:07:18 +02:00 committed by GitHub
parent 6a37600936
commit 5930f056a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1107 additions and 124 deletions

View file

@ -11,7 +11,7 @@ from typing import Any, cast
import jinja2
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import config as conf_util, config_entries
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -20,10 +20,9 @@ from homeassistant.const import (
CONF_PAYLOAD,
CONF_PORT,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
)
from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.helpers import config_validation as cv, event, template
@ -65,9 +64,10 @@ from .const import ( # noqa: F401
CONF_TOPIC,
CONF_WILL_MESSAGE,
CONFIG_ENTRY_IS_SETUP,
DATA_CONFIG_ENTRY_LOCK,
DATA_MQTT,
DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_DISPATCHERS,
DATA_MQTT_RELOAD_ENTRY,
DATA_MQTT_RELOAD_NEEDED,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING,
@ -87,7 +87,12 @@ from .models import ( # noqa: F401
ReceiveMessage,
ReceivePayloadType,
)
from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic
from .util import (
_VALID_QOS_SCHEMA,
mqtt_config_entry_enabled,
valid_publish_topic,
valid_subscribe_topic,
)
_LOGGER = logging.getLogger(__name__)
@ -174,7 +179,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
conf = dict(conf)
hass.data[DATA_MQTT_CONFIG] = conf
if not bool(hass.config_entries.async_entries(DOMAIN)):
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None:
# Create an import flow if the user has yaml configured entities etc.
# but no broker configuration. Note: The intention is not for this to
# import broker configuration from YAML because that has been deprecated.
@ -185,6 +190,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
data={},
)
)
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
elif mqtt_entry_status is False:
_LOGGER.info(
"MQTT will be not available until the config entry is enabled",
)
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
return True
@ -239,17 +251,18 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
await _async_setup_discovery(hass, mqtt_client.conf, entry)
async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Load a config entry."""
# Merge basic configuration, and add missing defaults for basic options
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None:
"""Fetch fresh MQTT yaml config from the hass config when (re)loading the entry."""
if DATA_MQTT_RELOAD_ENTRY in hass.data:
hass_config = await conf_util.async_hass_config_yaml(hass)
mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
hass.data[DATA_MQTT_CONFIG] = mqtt_config
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
# Bail out if broker setting is missing
if CONF_BROKER not in entry.data:
_LOGGER.error("MQTT broker is not configured, please configure it")
return False
return None
# If user doesn't have configuration.yaml config, generate default values
# for options not in config entry data
@ -271,22 +284,21 @@ async def async_setup_entry( # noqa: C901
# Merge advanced configuration values from configuration.yaml
conf = _merge_extended_config(entry, conf)
return conf
hass.data[DATA_MQTT] = MQTT(
hass,
entry,
conf,
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
# Merge basic configuration, and add missing defaults for basic options
if (conf := await async_fetch_config(hass, entry)) is None:
# Bail out
return False
hass.data[DATA_MQTT] = MQTT(hass, entry, conf)
entry.add_update_listener(_async_config_entry_updated)
await hass.data[DATA_MQTT].async_connect()
async def async_stop_mqtt(_event: Event):
"""Stop MQTT component."""
await hass.data[DATA_MQTT].async_disconnect()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt)
async def async_publish_service(call: ServiceCall) -> None:
"""Handle MQTT publish service calls."""
msg_topic = call.data.get(ATTR_TOPIC)
@ -375,7 +387,6 @@ async def async_setup_entry( # noqa: C901
)
# setup platforms and discovery
hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
async def async_setup_reload_service() -> None:
@ -411,6 +422,7 @@ async def async_setup_entry( # noqa: C901
# pylint: disable-next=import-outside-toplevel
from . import device_automation, tag
# Forward the entry setup to the MQTT platforms
await asyncio.gather(
*(
[
@ -428,21 +440,25 @@ async def async_setup_entry( # noqa: C901
await _async_setup_discovery(hass, conf, entry)
# Setup reload service after all platforms have loaded
await async_setup_reload_service()
if DATA_MQTT_RELOAD_NEEDED in hass.data:
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=False,
)
await async_reload_manual_mqtt_items(hass)
await async_forward_entry_setup_and_setup_discovery(entry)
return True
async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None:
"""Reload manual configured MQTT items."""
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
@websocket_api.websocket_command(
{vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str}
)
@ -544,3 +560,49 @@ async def async_remove_config_entry_device(
await device_automation.async_removed_from_device(hass, device_entry.id)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload MQTT dump and publish service when the config entry is unloaded."""
# Unload publish and dump services.
hass.services.async_remove(
DOMAIN,
SERVICE_PUBLISH,
)
hass.services.async_remove(
DOMAIN,
SERVICE_DUMP,
)
# Stop the discovery
await discovery.async_stop(hass)
mqtt_client: MQTT = hass.data[DATA_MQTT]
# Unload the platforms
await asyncio.gather(
*(
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
)
)
await hass.async_block_till_done()
# Unsubscribe reload dispatchers
while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []):
reload_dispatchers.pop()()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
# Cleanup listeners
mqtt_client.cleanup()
# Trigger reload manual MQTT items at entry setup
# Reload the legacy yaml platform
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False:
# The entry is disabled reload legacy manual items when the entry is enabled again
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
elif mqtt_entry_status is True:
# The entry is reloaded:
# Trigger re-fetching the yaml config at entry setup
hass.data[DATA_MQTT_RELOAD_ENTRY] = True
# Stop the loop
await mqtt_client.async_disconnect()
return True

View file

@ -156,8 +156,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])

View file

@ -112,8 +112,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT binary sensor."""
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])

View file

@ -91,8 +91,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT button."""
async_add_entities([MqttButton(hass, config, config_entry, discovery_data)])

View file

@ -89,8 +89,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT Camera."""
async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)])

View file

@ -23,8 +23,9 @@ from homeassistant.const import (
CONF_PROTOCOL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import CoreState, HassJob, HomeAssistant, callback
from homeassistant.core import CoreState, Event, HassJob, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.typing import ConfigType
@ -59,6 +60,7 @@ from .models import (
ReceiveMessage,
ReceivePayloadType,
)
from .util import mqtt_config_entry_enabled
if TYPE_CHECKING:
# Only import for paho-mqtt type checking here, imports are done locally
@ -95,6 +97,10 @@ async def async_publish(
) -> None:
"""Publish message to a MQTT topic."""
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
raise HomeAssistantError(
f"Cannot publish to topic '{topic}', MQTT is not enabled"
)
outgoing_payload = payload
if not isinstance(payload, bytes):
if not encoding:
@ -174,6 +180,10 @@ async def async_subscribe(
Call the return value to unsubscribe.
"""
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
raise HomeAssistantError(
f"Cannot subscribe to topic '{topic}', MQTT is not enabled"
)
# Count callback parameters which don't have a default value
non_default = 0
if msg_callback:
@ -316,6 +326,8 @@ class MQTT:
self._last_subscribe = time.time()
self._mqttc: mqtt.Client = None
self._paho_lock = asyncio.Lock()
self._pending_acks: set[int] = set()
self._cleanup_on_unload: list[Callable] = []
self._pending_operations: dict[str, asyncio.Event] = {}
@ -331,6 +343,20 @@ class MQTT:
self.init_client()
@callback
async def async_stop_mqtt(_event: Event):
"""Stop MQTT component."""
await self.async_disconnect()
self._cleanup_on_unload.append(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt)
)
def cleanup(self):
"""Clean up listeners."""
while self._cleanup_on_unload:
self._cleanup_on_unload.pop()()
def init_client(self):
"""Initialize paho client."""
self._mqttc = MqttClientSetup(self.conf).client
@ -405,6 +431,15 @@ class MQTT:
# Do not disconnect, we want the broker to always publish will
self._mqttc.loop_stop()
# wait for ACK-s to be processes (unsubscribe only)
async with self._paho_lock:
tasks = [
self.hass.async_create_task(self._wait_for_mid(mid))
for mid in self._pending_acks
]
await asyncio.gather(*tasks)
# stop the MQTT loop
await self.hass.async_add_executor_job(stop)
async def async_subscribe(
@ -440,7 +475,7 @@ class MQTT:
self.subscriptions.remove(subscription)
self._matching_subscriptions.cache_clear()
# Only unsubscribe if currently connected.
# Only unsubscribe if currently connected
if self.connected:
self.hass.async_create_task(self._async_unsubscribe(topic))
@ -451,18 +486,20 @@ class MQTT:
This method is a coroutine.
"""
def _client_unsubscribe(topic: str) -> None:
result: int | None = None
result, mid = self._mqttc.unsubscribe(topic)
_LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid)
_raise_on_error(result)
self._pending_acks.add(mid)
if any(other.topic == topic for other in self.subscriptions):
# Other subscriptions on topic remaining - don't unsubscribe.
return
async with self._paho_lock:
result: int | None = None
result, mid = await self.hass.async_add_executor_job(
self._mqttc.unsubscribe, topic
)
_LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid)
_raise_on_error(result)
await self._wait_for_mid(mid)
await self.hass.async_add_executor_job(_client_unsubscribe, topic)
async def _async_perform_subscriptions(
self, subscriptions: Iterable[tuple[str, int]]
@ -643,6 +680,10 @@ class MQTT:
)
finally:
del self._pending_operations[mid]
# Cleanup ACK sync buffer
async with self._paho_lock:
if mid in self._pending_acks:
self._pending_acks.remove(mid)
async def _discovery_cooldown(self):
now = time.time()

View file

@ -401,8 +401,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT climate devices."""
async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)])

View file

@ -31,9 +31,11 @@ CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock"
DATA_MQTT = "mqtt"
DATA_MQTT_CONFIG = "mqtt_config"
MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy"
DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers"
DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"

View file

@ -251,8 +251,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT Cover."""
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])

View file

@ -2,7 +2,11 @@
import voluptuous as vol
from homeassistant.components import device_tracker
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY
from ..mixins import warn_for_legacy_schema
from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401
from .schema_discovery import async_setup_entry_from_discovery
@ -12,5 +16,20 @@ from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN)
)
# Legacy setup
async_setup_scanner = async_setup_scanner_from_yaml
async_setup_entry = async_setup_entry_from_discovery
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery."""
await async_setup_entry_from_discovery(hass, config_entry, async_add_entities)
# (re)load legacy service
if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data:
await async_setup_scanner_from_yaml(
hass, **hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY]
)

View file

@ -1,4 +1,6 @@
"""Support for tracking MQTT enabled devices."""
"""Support for tracking MQTT enabled devices identified through discovery."""
from __future__ import annotations
import asyncio
import functools
@ -7,6 +9,7 @@ import voluptuous as vol
from homeassistant.components import device_tracker
from homeassistant.components.device_tracker import SOURCE_TYPES
from homeassistant.components.device_tracker.config_entry import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
@ -16,8 +19,10 @@ from homeassistant.const import (
STATE_HOME,
STATE_NOT_HOME,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from .. import subscription
from ..config import MQTT_RO_SCHEMA
@ -47,7 +52,11 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities):
async def async_setup_entry_from_discovery(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await asyncio.gather(
@ -66,8 +75,12 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT Device Tracker entity."""
async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)])

View file

@ -1,16 +1,23 @@
"""Support for tracking MQTT enabled devices defined in YAML."""
from collections.abc import Callable
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES
from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from ... import mqtt
from ..client import async_subscribe
from ..config import SCHEMA_BASE
from ..const import CONF_QOS
from ..util import valid_subscribe_topic
from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY
from ..util import mqtt_config_entry_enabled, valid_subscribe_topic
_LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_HOME = "payload_home"
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
@ -26,13 +33,34 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend(
)
async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None):
async def async_setup_scanner_from_yaml(
hass: HomeAssistant, config, async_see, discovery_info=None
):
"""Set up the MQTT tracker."""
devices = config[CONF_DEVICES]
qos = config[CONF_QOS]
payload_home = config[CONF_PAYLOAD_HOME]
payload_not_home = config[CONF_PAYLOAD_NOT_HOME]
source_type = config.get(CONF_SOURCE_TYPE)
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
subscriptions: list[Callable] = []
hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = {
"async_see": async_see,
"config": config,
}
if not mqtt_config_entry_enabled(hass):
_LOGGER.info(
"MQTT device trackers will be not available until the config entry is enabled",
)
return
@callback
def _entry_unload(*_: Any) -> None:
"""Handle the unload of the config entry."""
# Unsubscribe from mqtt
for unsubscribe in subscriptions:
unsubscribe()
for dev_id, topic in devices.items():
@ -52,6 +80,10 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=
hass.async_create_task(async_see(**see_args))
await async_subscribe(hass, topic, async_message_received, qos)
subscriptions.append(
await async_subscribe(hass, topic, async_message_received, qos)
)
config_entry.async_on_unload(_entry_unload)
return True

View file

@ -234,8 +234,7 @@ async def async_start( # noqa: C901
hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None
)
hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock()
hass.data.setdefault(DATA_CONFIG_FLOW_LOCK, asyncio.Lock())
hass.data[ALREADY_DISCOVERED] = {}
hass.data[PENDING_DISCOVERED] = {}

View file

@ -241,8 +241,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT fan."""
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])

View file

@ -197,8 +197,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT humidifier."""
async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)])

View file

@ -1,6 +1,7 @@
"""Support for MQTT lights."""
from __future__ import annotations
from collections.abc import Callable
import functools
import voluptuous as vol
@ -120,10 +121,14 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up a MQTT Light."""
setup_entity = {
setup_entity: dict[str, Callable] = {
"basic": async_setup_entity_basic,
"json": async_setup_entity_json,
"template": async_setup_entity_template,

View file

@ -112,8 +112,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT Lock platform."""
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])

View file

@ -3,7 +3,8 @@ from __future__ import annotations
from abc import abstractmethod
import asyncio
from collections.abc import Callable
from collections.abc import Callable, Coroutine
from functools import partial
import logging
from typing import Any, Protocol, cast, final
@ -61,7 +62,7 @@ from .const import (
CONF_TOPIC,
DATA_MQTT,
DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_NEEDED,
DATA_MQTT_RELOAD_DISPATCHERS,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING,
DEFAULT_PAYLOAD_AVAILABLE,
@ -84,7 +85,7 @@ from .subscription import (
async_subscribe_topics,
async_unsubscribe_topics,
)
from .util import valid_subscribe_topic
from .util import mqtt_config_entry_enabled, valid_subscribe_topic
_LOGGER = logging.getLogger(__name__)
@ -299,11 +300,24 @@ async def async_get_platform_config_from_yaml(
return platform_configs
async def async_setup_entry_helper(hass, domain, async_setup, schema):
async def async_setup_entry_helper(
hass: HomeAssistant,
domain: str,
async_setup: partial[Coroutine[HomeAssistant, str, None]],
schema: vol.Schema,
) -> None:
"""Set up entity, automation or tag creation dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add an MQTT entity, automation or tag."""
if not mqtt_config_entry_enabled(hass):
_LOGGER.warning(
"MQTT integration is disabled, skipping setup of discovered item "
"MQTT %s, payload %s",
domain,
discovery_payload,
)
return
discovery_data = discovery_payload.discovery_data
try:
config = schema(discovery_payload)
@ -316,8 +330,10 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema):
)
raise
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append(
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
)
)
@ -328,16 +344,17 @@ async def async_setup_platform_helper(
async_add_entities: AddEntitiesCallback,
async_setup_entities: SetupEntity,
) -> None:
"""Return true if platform setup should be aborted."""
if not bool(hass.config_entries.async_entries(DOMAIN)):
hass.data[DATA_MQTT_RELOAD_NEEDED] = None
"""Help to set up the platform for manual configured MQTT entities."""
if not (entry_status := mqtt_config_entry_enabled(hass)):
_LOGGER.warning(
"MQTT integration is not setup, skipping setup of manually configured "
"MQTT %s",
"MQTT integration is %s, skipping setup of manually configured MQTT %s",
"not setup" if entry_status is None else "disabled",
platform_domain,
)
return
await async_setup_entities(hass, async_add_entities, config)
# Ensure we set config_entry when entries are set up to enable clean up
config_entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
await async_setup_entities(hass, async_add_entities, config, config_entry)
def init_entity_id_from_config(hass, entity, config, entity_id_format):
@ -640,6 +657,7 @@ class MqttDiscoveryDeviceUpdate:
MQTT_DISCOVERY_UPDATED.format(discovery_hash),
self.async_discovery_update,
)
config_entry.async_on_unload(self._entry_unload)
if device_id is not None:
self._remove_device_updated = hass.bus.async_listen(
EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed
@ -650,6 +668,14 @@ class MqttDiscoveryDeviceUpdate:
discovery_hash,
)
@callback
def _entry_unload(self, *_: Any) -> None:
"""Handle cleanup when the config entry is unloaded."""
stop_discovery_updates(
self.hass, self._discovery_data, self._remove_discovery_updated
)
self.hass.async_add_job(self.async_tear_down())
async def async_discovery_update(
self,
discovery_payload: DiscoveryInfoType | None,
@ -734,7 +760,11 @@ class MqttDiscoveryDeviceUpdate:
class MqttDiscoveryUpdate(Entity):
"""Mixin used to handle updated discovery message for entity based platforms."""
def __init__(self, discovery_data, discovery_update=None) -> None:
def __init__(
self,
discovery_data: dict,
discovery_update: Callable | None = None,
) -> None:
"""Initialize the discovery update mixin."""
self._discovery_data = discovery_data
self._discovery_update = discovery_update

View file

@ -145,8 +145,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT number."""
async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)])

View file

@ -88,8 +88,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT scene."""
async_add_entities([MqttScene(hass, config, config_entry, discovery_data)])

View file

@ -103,8 +103,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT select."""
async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)])

View file

@ -156,8 +156,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up MQTT sensor."""
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])

View file

@ -152,8 +152,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT siren."""
async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)])

View file

@ -111,8 +111,12 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT switch."""
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])

View file

@ -1,9 +1,13 @@
"""Utility functions for the MQTT integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, template
from .const import (
@ -13,9 +17,17 @@ from .const import (
ATTR_TOPIC,
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
)
def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None:
"""Return true when the MQTT config entry is enabled."""
if not bool(hass.config_entries.async_entries(DOMAIN)):
return None
return not bool(hass.config_entries.async_entries(DOMAIN)[0].disabled_by)
def valid_topic(value: Any) -> str:
"""Validate that this is a valid topic name/filter."""
value = cv.string(value)

View file

@ -100,10 +100,17 @@ async def async_setup_entry(
async def _async_setup_entity(
hass, async_add_entities, config, config_entry=None, discovery_data=None
):
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry | None = None,
discovery_data: dict | None = None,
) -> None:
"""Set up the MQTT vacuum."""
setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state}
setup_entity = {
LEGACY: async_setup_entity_legacy,
STATE: async_setup_entity_state,
}
await setup_entity[config[CONF_SCHEMA]](
hass, config, async_add_entities, config_entry, discovery_data
)

View file

@ -60,6 +60,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -971,3 +972,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = alarm_control_panel.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -44,6 +44,7 @@ from .test_common import (
help_test_setting_attribute_with_template,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1079,3 +1080,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = binary_sensor.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -37,6 +37,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -482,3 +483,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = button.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -36,6 +36,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -346,3 +347,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = camera.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -55,6 +55,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1881,3 +1882,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = climate.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -2,7 +2,7 @@
import copy
from datetime import datetime
import json
from unittest.mock import ANY, patch
from unittest.mock import ANY, MagicMock, patch
import yaml
@ -11,6 +11,7 @@ from homeassistant.components import mqtt
from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ENTITY_ID,
@ -1670,6 +1671,25 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config):
assert "<Event event_mqtt_reloaded[L]>" in caplog.text
async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config):
"""Test reloading with supplied config."""
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert mqtt_config_entry.state is ConfigEntryState.LOADED
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump(new_config)
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch(
"paho.mqtt.client.Client"
) as mock_client:
mock_client().connect = lambda *args: 0
# reload the config entry
assert await hass.config_entries.async_reload(mqtt_config_entry.entry_id)
assert mqtt_config_entry.state is ConfigEntryState.LOADED
await hass.async_block_till_done()
async def help_test_reloadable(
hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config
):
@ -1782,6 +1802,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config):
domain: [new_config_1, new_config_2, new_config_3],
}
await help_test_reload_with_config(hass, caplog, tmp_path, new_config)
await hass.async_block_till_done()
assert len(hass.states.async_all(domain)) == 3
@ -1792,6 +1813,12 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config):
async def help_test_setup_manual_entity_from_yaml(hass, platform, config):
"""Help to test setup from yaml through configuration entry."""
calls = MagicMock()
async def mock_reload(hass):
"""Mock reload."""
calls()
config_structure = {mqtt.DOMAIN: {platform: config}}
await async_setup_component(hass, mqtt.DOMAIN, config_structure)
@ -1799,7 +1826,71 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config):
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass)
with patch("paho.mqtt.client.Client") as mock_client:
with patch(
"homeassistant.components.mqtt.async_reload_manual_mqtt_items",
side_effect=mock_reload,
), patch("paho.mqtt.client.Client") as mock_client:
mock_client().connect = lambda *args: 0
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
calls.assert_called_once()
async def help_test_unload_config_entry(hass, tmp_path, newconfig):
"""Test unloading the MQTT config entry."""
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert mqtt_config_entry.state is ConfigEntryState.LOADED
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump(newconfig)
new_yaml_config_file.write_text(new_yaml_config)
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id)
assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED
await hass.async_block_till_done()
async def help_test_unload_config_entry_with_platform(
hass,
mqtt_mock_entry_with_yaml_config,
tmp_path,
domain,
config,
):
"""Test unloading the MQTT config entry with a specific platform domain."""
# prepare setup through configuration.yaml
config_setup = copy.deepcopy(config)
config_setup["name"] = "config_setup"
config_name = config_setup
assert await async_setup_component(hass, domain, {domain: [config_setup]})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
# prepare setup through discovery
discovery_setup = copy.deepcopy(config)
discovery_setup["name"] = "discovery_setup"
async_fire_mqtt_message(
hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup)
)
await hass.async_block_till_done()
# check if both entities were setup correctly
config_setup_entity = hass.states.get(f"{domain}.config_setup")
assert config_setup_entity
discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup")
assert discovery_setup_entity
await help_test_unload_config_entry(hass, tmp_path, config_setup)
async_fire_mqtt_message(
hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup)
)
await hass.async_block_till_done()
# check if both entities were unloaded correctly
config_setup_entity = hass.states.get(f"{domain}.{config_name}")
assert config_setup_entity is None
discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup")
assert discovery_setup_entity is None

View file

@ -145,12 +145,17 @@ async def test_manual_config_starts_discovery_flow(
async def test_manual_config_set(
hass, mock_try_connection, mock_finish_setup, mqtt_client_mock
hass,
mock_try_connection,
mock_finish_setup,
mqtt_client_mock,
):
"""Test manual config does not create an entry, and entry can be setup late."""
# MQTT config present in yaml config
assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}})
await hass.async_block_till_done()
# do not try to reload
del hass.data["mqtt_reload_needed"]
assert len(mock_finish_setup.mock_calls) == 0
mock_try_connection.return_value = True

View file

@ -73,6 +73,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -3364,3 +3365,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = cover.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -1,13 +1,20 @@
"""The tests for the MQTT device tracker platform using configuration.yaml."""
import json
from unittest.mock import patch
import pytest
from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH
from homeassistant.config_entries import ConfigEntryDisabler
from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform
from homeassistant.setup import async_setup_component
from .test_common import help_test_setup_manual_entity_from_yaml
from .test_common import (
MockConfigEntry,
help_test_entry_reload_with_new_config,
help_test_setup_manual_entity_from_yaml,
help_test_unload_config_entry,
)
from tests.common import async_fire_mqtt_message
@ -265,3 +272,114 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf):
await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config)
assert hass.states.get(entity_id) is not None
async def test_unload_entry(
hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path
):
"""Test unloading the config entry."""
# setup through configuration.yaml
await mqtt_mock_entry_no_yaml_config()
dev_id = "jan"
entity_id = f"{DOMAIN}.{dev_id}"
topic = "/location/jan"
location = "home"
hass.config.components = {"mqtt", "zone"}
assert await async_setup_component(
hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}
)
async_fire_mqtt_message(hass, topic, location)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == location
# setup through discovery
dev_id = "piet"
subscription = "/location/#"
domain = DOMAIN
discovery_config = {
"devices": {dev_id: subscription},
"state_topic": "some-state",
"name": "piet",
}
async_fire_mqtt_message(
hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_config)
)
await hass.async_block_till_done()
# check that both entities were created
config_setup_entity = hass.states.get(f"{domain}.jan")
assert config_setup_entity
discovery_setup_entity = hass.states.get(f"{domain}.piet")
assert discovery_setup_entity
await help_test_unload_config_entry(hass, tmp_path, {})
await hass.async_block_till_done()
# check that both entities were unsubscribed and that the location was not processed
async_fire_mqtt_message(hass, "some-state", "not_home")
async_fire_mqtt_message(hass, "location/jan", "not_home")
await hass.async_block_till_done()
config_setup_entity = hass.states.get(f"{domain}.jan")
assert config_setup_entity.state == location
# the discovered tracker is an entity which state is removed at unload
discovery_setup_entity = hass.states.get(f"{domain}.piet")
assert discovery_setup_entity is None
async def test_reload_entry_legacy(
hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path
):
"""Test reloading the config entry with manual MQTT items."""
# setup through configuration.yaml
await mqtt_mock_entry_no_yaml_config()
entity_id = f"{DOMAIN}.jan"
topic = "location/jan"
location = "home"
config = {
DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}},
}
hass.config.components = {"mqtt", "zone"}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, topic, location)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == location
await help_test_entry_reload_with_new_config(hass, tmp_path, config)
await hass.async_block_till_done()
location = "not_home"
async_fire_mqtt_message(hass, topic, location)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == location
async def test_setup_with_disabled_entry(
hass, mock_device_tracker_conf, caplog
) -> None:
"""Test setting up the platform with a disabled config entry."""
# Try to setup the platform with a disabled config entry
config_entry = MockConfigEntry(
domain="mqtt", data={}, disabled_by=ConfigEntryDisabler.USER
)
config_entry.add_to_hass(hass)
topic = "location/jan"
config = {
DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}},
}
hass.config.components = {"mqtt", "zone"}
await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert (
"MQTT device trackers will be not available until the config entry is enabled"
in caplog.text
)

View file

@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.setup import async_setup_component
from .test_common import help_test_unload_config_entry
from tests.common import (
assert_lists_same,
async_fire_mqtt_message,
@ -1372,3 +1374,53 @@ async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config):
== "homeassistant/device_automation/bla2/config"
)
assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2
async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> None:
"""Test unloading the MQTT entry."""
data1 = (
'{ "automation_type":"trigger",'
' "device":{"identifiers":["0AFFD2"]},'
' "topic": "foobar/triggers/button1",'
' "type": "button_short_press",'
' "subtype": "button_1" }'
)
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
await hass.async_block_till_done()
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"discovery_id": "bla1",
"type": "button_short_press",
"subtype": "button_1",
},
"action": {
"service": "test.automation",
"data_template": {"some": ("short_press")},
},
},
]
},
)
# Fake short press 1
async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
await hass.async_block_till_done()
assert len(calls) == 1
await help_test_unload_config_entry(hass, tmp_path, {})
# Fake short press 2
async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
await hass.async_block_till_done()
assert len(calls) == 1

View file

@ -58,6 +58,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1910,3 +1911,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = fan.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -60,6 +60,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1296,3 +1297,12 @@ async def test_config_schema_validation(hass):
CONFIG_SCHEMA({DOMAIN: {platform: [config]}})
with pytest.raises(MultipleInvalid):
CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}})
async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = humidifier.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -17,6 +17,7 @@ from homeassistant.components import mqtt
from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info
from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA
from homeassistant.components.mqtt.models import ReceiveMessage
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import (
ATTR_ASSUMED_STATE,
EVENT_HOMEASSISTANT_STARTED,
@ -32,7 +33,10 @@ from homeassistant.helpers.entity import Entity
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .test_common import help_test_setup_manual_entity_from_yaml
from .test_common import (
help_test_entry_reload_with_new_config,
help_test_setup_manual_entity_from_yaml,
)
from tests.common import (
MockConfigEntry,
@ -106,6 +110,18 @@ def record_calls(calls):
return record_calls
@pytest.fixture
def empty_mqtt_config(hass, tmp_path):
"""Fixture to provide an empty config from yaml."""
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config_file.write_text("")
with patch.object(
hass_config, "YAML_CONFIG_FILE", new_yaml_config_file
) as empty_config:
yield empty_config
async def test_mqtt_connects_on_home_assistant_mqtt_setup(
hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config
):
@ -115,14 +131,14 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup(
async def test_mqtt_disconnects_on_home_assistant_stop(
hass, mqtt_mock_entry_no_yaml_config
hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock
):
"""Test if client stops on HA stop."""
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
await mqtt_mock_entry_no_yaml_config()
hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert mqtt_mock.async_disconnect.called
assert mqtt_client_mock.loop_stop.call_count == 1
async def test_publish(hass, mqtt_mock_entry_no_yaml_config):
@ -521,8 +537,11 @@ async def test_service_call_with_ascii_qos_retain_flags(
assert not mqtt_mock.async_publish.call_args[0][3]
async def test_publish_function_with_bad_encoding_conditions(hass, caplog):
"""Test internal publish function with bas use cases."""
async def test_publish_function_with_bad_encoding_conditions(
hass, caplog, mqtt_mock_entry_no_yaml_config
):
"""Test internal publish function with basic use cases."""
await mqtt_mock_entry_no_yaml_config()
await mqtt.async_publish(
hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None
)
@ -1249,13 +1268,18 @@ async def test_restore_all_active_subscriptions_on_reconnect(
assert mqtt_client_mock.subscribe.mock_calls == expected
async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock):
async def test_initial_setup_logs_error(
hass, caplog, mqtt_client_mock, empty_mqtt_config
):
"""Test for setup failure if initial client connection fails."""
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass)
mqtt_client_mock.connect.return_value = 1
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
try:
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
except HomeAssistantError:
assert True
assert "Failed to connect to MQTT server:" in caplog.text
@ -1298,6 +1322,7 @@ async def test_handle_mqtt_on_callback(
async def test_publish_error(hass, caplog):
"""Test publish error."""
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass)
# simulate an Out of memory error
with patch("paho.mqtt.client.Client") as mock_client:
@ -1365,6 +1390,7 @@ async def test_setup_override_configuration(hass, caplog, tmp_path):
domain=mqtt.DOMAIN,
data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"},
)
entry.add_to_hass(hass)
with patch("paho.mqtt.client.Client") as mock_client:
mock_client().username_pw_set = mock_usename_password_set
@ -1429,9 +1455,11 @@ async def test_setup_mqtt_client_protocol(hass):
mqtt.config_integration.CONF_PROTOCOL: "3.1",
},
)
entry.add_to_hass(hass)
with patch("paho.mqtt.client.Client") as mock_client:
mock_client.on_connect(return_value=0)
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
# check if protocol setup was correctly
assert mock_client.call_args[1]["protocol"] == 3
@ -1467,15 +1495,18 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog):
entry = MockConfigEntry(
domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}
)
# Set up the integration
assert await mqtt.async_setup_entry(hass, entry)
entry.add_to_hass(hass)
# Make sure we are connected correctly
mock_client.on_connect(mock_client, None, None, 0)
# Set up the integration
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
# Now call we publish without simulating and ACK callback
await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload")
await hass.async_block_till_done()
# The is no ACK so we should see a timeout in the log after publishing
# There is no ACK so we should see a timeout in the log after publishing
assert len(mock_client.publish.mock_calls) == 1
assert "No ACK from MQTT server" in caplog.text
@ -1483,10 +1514,12 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog):
async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog):
"""Test for setup failure if connection to broker is missing."""
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass)
with patch("paho.mqtt.client.Client") as mock_client:
mock_client().connect = MagicMock(side_effect=OSError("Connection error"))
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
assert "Failed to connect to MQTT server due to exception:" in caplog.text
@ -1514,8 +1547,9 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure(
domain=mqtt.DOMAIN,
data=config_item_data,
)
entry.add_to_hass(hass)
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
assert calls
@ -1546,8 +1580,9 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
domain=mqtt.DOMAIN,
data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"},
)
entry.add_to_hass(hass)
assert await mqtt.async_setup_entry(hass, entry)
await hass.async_block_till_done()
assert calls
@ -2644,3 +2679,206 @@ async def test_config_schema_validation(hass):
config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}}
with pytest.raises(vol.MultipleInvalid):
CONFIG_SCHEMA(config)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
async def test_unload_config_entry(
hass, mqtt_mock, mqtt_client_mock, tmp_path, caplog
) -> None:
"""Test unloading the MQTT entry."""
assert hass.services.has_service(mqtt.DOMAIN, "dump")
assert hass.services.has_service(mqtt.DOMAIN, "publish")
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert mqtt_config_entry.state is ConfigEntryState.LOADED
# Publish just before unloading to test await cleanup
mqtt_client_mock.reset_mock()
mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False)
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump({})
new_yaml_config_file.write_text(new_yaml_config)
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id)
mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False)
assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED
await hass.async_block_till_done()
assert not hass.services.has_service(mqtt.DOMAIN, "dump")
assert not hass.services.has_service(mqtt.DOMAIN, "publish")
assert "No ACK from MQTT server" not in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [])
async def test_setup_with_disabled_entry(hass, caplog) -> None:
"""Test setting up the platform with a disabled config entry."""
# Try to setup the platform with a disabled config entry
config_entry = MockConfigEntry(
domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER
)
config_entry.add_to_hass(hass)
config = {mqtt.DOMAIN: {}}
await async_setup_component(hass, mqtt.DOMAIN, config)
await hass.async_block_till_done()
assert "MQTT will be not available until the config entry is enabled" in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [])
async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog):
"""Test internal publish function with bas use cases."""
with pytest.raises(HomeAssistantError):
await mqtt.async_publish(
hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None
)
with pytest.raises(HomeAssistantError):
await mqtt.async_subscribe(hass, "some-topic", lambda: None, qos=0)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
async def test_reload_entry_with_new_config(hass, tmp_path):
"""Test reloading the config entry with a new yaml config."""
config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}]
config_yaml_new = {
"mqtt": {
"light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}]
},
"light": [
{
"platform": "mqtt",
"name": "test_new_legacy",
"command_topic": "test-topic_new",
}
],
}
await help_test_setup_manual_entity_from_yaml(hass, "light", config_old)
assert hass.states.get("light.test_old1") is not None
await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new)
assert hass.states.get("light.test_old1") is None
assert hass.states.get("light.test_new_modern") is not None
assert hass.states.get("light.test_new_legacy") is not None
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
async def test_disabling_and_enabling_entry(hass, tmp_path, caplog):
"""Test disabling and enabling the config entry."""
config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}]
config_yaml_new = {
"mqtt": {
"light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}]
},
"light": [
{
"platform": "mqtt",
"name": "test_new_legacy",
"command_topic": "test-topic_new",
}
],
}
await help_test_setup_manual_entity_from_yaml(hass, "light", config_old)
assert hass.states.get("light.test_old1") is not None
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert mqtt_config_entry.state is ConfigEntryState.LOADED
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump(config_yaml_new)
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch(
"paho.mqtt.client.Client"
) as mock_client:
mock_client().connect = lambda *args: 0
# Late discovery of a light
config = '{"name": "abc", "command_topic": "test-topic"}'
async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config)
# Disable MQTT config entry
await hass.config_entries.async_set_disabled_by(
mqtt_config_entry.entry_id, ConfigEntryDisabler.USER
)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Assert that the discovery was still received
# but kipped the setup
assert (
"MQTT integration is disabled, skipping setup of manually configured MQTT light"
in caplog.text
)
assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED
assert hass.states.get("light.test_old1") is None
# Enable the entry again
await hass.config_entries.async_set_disabled_by(
mqtt_config_entry.entry_id, None
)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert mqtt_config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("light.test_old1") is None
assert hass.states.get("light.test_new_modern") is not None
assert hass.states.get("light.test_new_legacy") is not None
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
@pytest.mark.parametrize(
"config, unique",
[
(
[
{
"name": "test1",
"unique_id": "very_not_unique_deadbeef",
"command_topic": "test-topic_unique",
},
{
"name": "test2",
"unique_id": "very_not_unique_deadbeef",
"command_topic": "test-topic_unique",
},
],
False,
),
(
[
{
"name": "test1",
"unique_id": "very_unique_deadbeef1",
"command_topic": "test-topic_unique",
},
{
"name": "test2",
"unique_id": "very_unique_deadbeef2",
"command_topic": "test-topic_unique",
},
],
True,
),
],
)
async def test_setup_manual_items_with_unique_ids(
hass, tmp_path, caplog, config, unique
):
"""Test setup manual items is generating unique id's."""
await help_test_setup_manual_entity_from_yaml(hass, "light", config)
assert hass.states.get("light.test1") is not None
assert (hass.states.get("light.test2") is not None) == unique
assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique
# reload and assert again
caplog.clear()
await help_test_entry_reload_with_new_config(
hass, tmp_path, {"mqtt": {"light": config}}
)
assert hass.states.get("light.test1") is not None
assert (hass.states.get("light.test2") is not None) == unique
assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique

View file

@ -240,6 +240,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -3803,3 +3804,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = light.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -72,6 +72,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1266,3 +1267,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = light.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -48,6 +48,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -748,3 +749,12 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = LOCK_DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -57,6 +57,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -853,3 +854,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = number.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -22,6 +22,7 @@ from .test_common import (
help_test_reloadable_late,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
)
DEFAULT_CONFIG = {
@ -237,3 +238,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = scene.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -48,6 +48,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -687,3 +688,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = select.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -58,6 +58,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -1213,3 +1214,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = sensor.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -45,6 +45,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -975,3 +976,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = siren.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -42,6 +42,7 @@ from .test_common import (
help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id,
help_test_unload_config_entry_with_platform,
help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict,
)
@ -664,3 +665,12 @@ async def test_setup_manual_entity_from_yaml(hass):
del config["platform"]
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
assert hass.states.get(f"{platform}.test") is not None
async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
"""Test unloading the config entry."""
domain = switch.DOMAIN
config = DEFAULT_CONFIG[domain]
await help_test_unload_config_entry_with_platform(
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
)

View file

@ -11,6 +11,8 @@ from homeassistant.const import Platform
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .test_common import help_test_unload_config_entry
from tests.common import (
MockConfigEntry,
async_fire_mqtt_message,
@ -797,3 +799,28 @@ async def test_cleanup_device_with_entity2(
# Verify device registry entry is cleared
device_entry = device_reg.async_get_device({("mqtt", "helloworld")})
assert device_entry is None
async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None:
"""Test unloading the MQTT entry."""
config = copy.deepcopy(DEFAULT_CONFIG_DEVICE)
async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config))
await hass.async_block_till_done()
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
# Fake tag scan, should be processed
async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
await hass.async_block_till_done()
tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
tag_mock.reset_mock()
await help_test_unload_config_entry(hass, tmp_path, {})
await hass.async_block_till_done()
# Fake tag scan, should not be processed
async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
await hass.async_block_till_done()
tag_mock.assert_not_called()