diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cced3670cca..eb1e8c01ae0 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -35,7 +35,11 @@ from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass @@ -78,6 +82,7 @@ from .const import ( from .debug_info import log_messages from .discovery import ( LAST_DISCOVERY, + MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash, @@ -1315,6 +1320,9 @@ class MqttDiscoveryUpdate(Entity): else: # Non-empty, unchanged payload: Ignore to avoid changing states _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) if discovery_hash: debug_info.add_entity_discovery_data( @@ -1327,17 +1335,26 @@ class MqttDiscoveryUpdate(Entity): MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_callback, ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass: discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish( - self.hass, - discovery_topic, - "", - retain=True, + publish(self.hass, discovery_topic, "", retain=True) + + @callback + def add_to_platform_abort(self) -> None: + """Abort adding an entity to a platform.""" + if self._discovery_data: + discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(self.hass, discovery_hash) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) + super().add_to_platform_abort() async def async_will_remove_from_hass(self) -> None: """Stop listening to signal and cleanup discovery data..""" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index edf383a6819..46eaa912615 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -29,7 +29,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -49,7 +52,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -119,7 +122,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index e081423d590..17dcd301cd0 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -21,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -42,7 +45,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -92,7 +95,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index e8783f74bd4..20d68d1c4b0 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -8,7 +8,10 @@ from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -25,7 +28,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -66,7 +69,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c5835f8e7c7..715658417b2 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -47,7 +47,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -66,7 +69,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -259,7 +262,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 25fcf0ad0d2..59dedd7f475 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -33,7 +33,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -53,7 +56,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -190,7 +193,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index c064cca599d..1186a212243 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -4,11 +4,14 @@ import logging import voluptuous as vol from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from . import ATTR_DISCOVERY_HASH, device_trigger from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -42,7 +45,11 @@ async def async_setup_entry(hass, config_entry): hass, config, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 4de2ae4fa6d..08a4871084c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -20,7 +20,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .. import ( MqttAttributes, @@ -32,7 +35,7 @@ from .. import ( from ... import mqtt from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -69,7 +72,11 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 9fa51bebf09..8a8b525da61 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -11,7 +11,10 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -28,7 +31,7 @@ from . import ( trigger as mqtt_trigger, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -206,6 +209,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] await device_trigger.update_trigger(config, discovery_hash, remove_signal) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) remove_signal = async_dispatcher_connect( hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update @@ -220,6 +224,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): ) if device is None: + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) return if DEVICE_TRIGGERS not in hass.data: @@ -244,6 +249,8 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): hass, discovery_hash, discovery_data, device.id ) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) + async def async_device_removed(hass: HomeAssistant, device_id: str): """Handle the removal of a device.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5452d15aa30..eec1a63f932 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,5 +1,6 @@ """Support for MQTT discovery.""" import asyncio +from collections import deque import functools import json import logging @@ -7,7 +8,10 @@ import re import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt @@ -46,6 +50,7 @@ SUPPORTED_COMPONENTS = [ ] ALREADY_DISCOVERED = "mqtt_discovered_components" +PENDING_DISCOVERED = "mqtt_pending_components" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" @@ -53,6 +58,7 @@ DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" +MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -78,7 +84,7 @@ async def async_start( """Start MQTT Discovery.""" mqtt_integrations = {} - async def async_entity_message_received(msg): + async def async_discovery_message_received(msg): """Process the received message.""" hass.data[LAST_DISCOVERY] = time.time() payload = msg.payload @@ -141,8 +147,46 @@ async def async_start( payload[CONF_PLATFORM] = "mqtt" - if ALREADY_DISCOVERED not in hass.data: - hass.data[ALREADY_DISCOVERED] = {} + if discovery_hash in hass.data[PENDING_DISCOVERED]: + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + pending.appendleft(payload) + _LOGGER.info( + "Component has already been discovered: %s %s, queuing update", + component, + discovery_id, + ) + return + + await async_process_discovery_payload(component, discovery_id, payload) + + async def async_process_discovery_payload(component, discovery_id, payload): + + _LOGGER.debug("Process discovery payload %s", payload) + discovery_hash = (component, discovery_id) + if discovery_hash in hass.data[ALREADY_DISCOVERED] or payload: + + async def discovery_done(_): + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + _LOGGER.debug("Pending discovery for %s: %s", discovery_hash, pending) + if not pending: + hass.data[PENDING_DISCOVERED][discovery_hash]["unsub"]() + hass.data[PENDING_DISCOVERED].pop(discovery_hash) + else: + payload = pending.pop() + await async_process_discovery_payload( + component, discovery_id, payload + ) + + if discovery_hash not in hass.data[PENDING_DISCOVERED]: + hass.data[PENDING_DISCOVERED][discovery_hash] = { + "unsub": async_dispatcher_connect( + hass, + MQTT_DISCOVERY_DONE.format(discovery_hash), + discovery_done, + ), + "pending": deque([]), + } + if discovery_hash in hass.data[ALREADY_DISCOVERED]: # Dispatch update _LOGGER.info( @@ -182,13 +226,21 @@ async def async_start( async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) + else: + # Unhandled discovery message + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() + hass.data[ALREADY_DISCOVERED] = {} + hass.data[PENDING_DISCOVERED] = {} + hass.data[DISCOVERY_UNSUBSCRIBE] = await mqtt.async_subscribe( - hass, f"{discovery_topic}/#", async_entity_message_received, 0 + hass, f"{discovery_topic}/#", async_discovery_message_received, 0 ) hass.data[LAST_DISCOVERY] = time.time() mqtt_integrations = await async_get_mqtt(hass) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 96d5fe720c3..cc635ab6e45 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -25,7 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -45,7 +48,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -131,7 +134,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 393cb2fcf13..1ab0888866c 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -4,12 +4,15 @@ import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json @@ -53,7 +56,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 712f2e0e376..70c771ba22d 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -14,7 +14,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -34,7 +37,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -93,7 +96,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 673eb169b19..eebcdc26bac 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -7,7 +7,10 @@ from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -22,7 +25,7 @@ from . import ( MqttDiscoveryUpdate, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -61,7 +64,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 1fda8986ef7..cfcd46c9e95 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -19,7 +19,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -40,7 +43,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -86,7 +89,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 76019680110..e074cb819d2 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -18,7 +18,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -39,7 +42,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -89,7 +92,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 75f3bb50309..1185f925b74 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -6,7 +6,10 @@ import voluptuous as vol from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from . import ( ATTR_DISCOVERY_HASH, @@ -21,7 +24,12 @@ from . import ( subscription, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .discovery import ( + MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_NEW, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -50,7 +58,11 @@ async def async_setup_entry(hass, config_entry): config = PLATFORM_SCHEMA(discovery_payload) await async_setup_tag(hass, config, config_entry, discovery_data) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( @@ -142,6 +154,10 @@ class MQTTTagScanner: self._setup_from_config(config) await self.subscribe_topics() + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + def _setup_from_config(self, config): self._value_template = lambda value, error_value: value if CONF_VALUE_TEMPLATE in config: @@ -163,6 +179,9 @@ class MQTTTagScanner: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.discovery_update, ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def subscribe_topics(self): """Subscribe to MQTT topics.""" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index f6265d1b96b..36e6df4ed1d 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -4,11 +4,14 @@ import logging import voluptuous as vol from homeassistant.components.vacuum import DOMAIN -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -45,7 +48,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 7f22e0eef6f..04d35ab1d26 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -12,7 +12,8 @@ from homeassistant.components.mqtt.abbreviations import ( DEVICE_ABBREVIATIONS, ) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +import homeassistant.core as ha from tests.common import ( async_fire_mqtt_message, @@ -252,6 +253,121 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None +async def test_rapid_rediscover(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.beer") + assert state is not None + assert len(events) == 1 + + # Removal immediately followed by rediscover + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 1 + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 5 + # Remove the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["new_state"] is None + # Add the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["old_state"] is None + # Remove the entity + assert events[3].data["entity_id"] == "binary_sensor.beer" + assert events[3].data["new_state"] is None + # Add the entity + assert events[4].data["entity_id"] == "binary_sensor.milk" + assert events[4].data["old_state"] is None + + +async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla2/config", + '{ "name": "Ale", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.ale") + assert state is not None + assert len(events) == 1 + + # Duplicate unique_id, immediately followed by correct unique_id + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 2 + state = hass.states.get("binary_sensor.ale") + assert state is not None + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 4 + # Add the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["old_state"] is None + # Remove the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["new_state"] is None + # Add the entity + assert events[3].data["entity_id"] == "binary_sensor.milk" + assert events[3].data["old_state"] is None + + async def test_duplicate_removal(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" async_fire_mqtt_message(