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:
parent
6a37600936
commit
5930f056a8
49 changed files with 1107 additions and 124 deletions
|
@ -11,7 +11,7 @@ from typing import Any, cast
|
||||||
import jinja2
|
import jinja2
|
||||||
import voluptuous as vol
|
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.components import websocket_api
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -20,10 +20,9 @@ from homeassistant.const import (
|
||||||
CONF_PAYLOAD,
|
CONF_PAYLOAD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
SERVICE_RELOAD,
|
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.data_entry_flow import BaseServiceInfo
|
||||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
from homeassistant.exceptions import TemplateError, Unauthorized
|
||||||
from homeassistant.helpers import config_validation as cv, event, template
|
from homeassistant.helpers import config_validation as cv, event, template
|
||||||
|
@ -65,9 +64,10 @@ from .const import ( # noqa: F401
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
CONF_WILL_MESSAGE,
|
CONF_WILL_MESSAGE,
|
||||||
CONFIG_ENTRY_IS_SETUP,
|
CONFIG_ENTRY_IS_SETUP,
|
||||||
DATA_CONFIG_ENTRY_LOCK,
|
|
||||||
DATA_MQTT,
|
DATA_MQTT,
|
||||||
DATA_MQTT_CONFIG,
|
DATA_MQTT_CONFIG,
|
||||||
|
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||||
|
DATA_MQTT_RELOAD_ENTRY,
|
||||||
DATA_MQTT_RELOAD_NEEDED,
|
DATA_MQTT_RELOAD_NEEDED,
|
||||||
DATA_MQTT_UPDATED_CONFIG,
|
DATA_MQTT_UPDATED_CONFIG,
|
||||||
DEFAULT_ENCODING,
|
DEFAULT_ENCODING,
|
||||||
|
@ -87,7 +87,12 @@ from .models import ( # noqa: F401
|
||||||
ReceiveMessage,
|
ReceiveMessage,
|
||||||
ReceivePayloadType,
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -174,7 +179,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
conf = dict(conf)
|
conf = dict(conf)
|
||||||
hass.data[DATA_MQTT_CONFIG] = 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.
|
# Create an import flow if the user has yaml configured entities etc.
|
||||||
# but no broker configuration. Note: The intention is not for this to
|
# but no broker configuration. Note: The intention is not for this to
|
||||||
# import broker configuration from YAML because that has been deprecated.
|
# import broker configuration from YAML because that has been deprecated.
|
||||||
|
@ -185,6 +190,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
data={},
|
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
|
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)
|
await _async_setup_discovery(hass, mqtt_client.conf, entry)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry( # noqa: C901
|
async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None:
|
||||||
hass: HomeAssistant, entry: ConfigEntry
|
"""Fetch fresh MQTT yaml config from the hass config when (re)loading the entry."""
|
||||||
) -> bool:
|
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||||
"""Load a config entry."""
|
hass_config = await conf_util.async_hass_config_yaml(hass)
|
||||||
# Merge basic configuration, and add missing defaults for basic options
|
mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
|
||||||
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
|
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
|
# Bail out if broker setting is missing
|
||||||
if CONF_BROKER not in entry.data:
|
if CONF_BROKER not in entry.data:
|
||||||
_LOGGER.error("MQTT broker is not configured, please configure it")
|
_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
|
# If user doesn't have configuration.yaml config, generate default values
|
||||||
# for options not in config entry data
|
# 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
|
# Merge advanced configuration values from configuration.yaml
|
||||||
conf = _merge_extended_config(entry, conf)
|
conf = _merge_extended_config(entry, conf)
|
||||||
|
return conf
|
||||||
|
|
||||||
hass.data[DATA_MQTT] = MQTT(
|
|
||||||
hass,
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
entry,
|
"""Load a config entry."""
|
||||||
conf,
|
# 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)
|
entry.add_update_listener(_async_config_entry_updated)
|
||||||
|
|
||||||
await hass.data[DATA_MQTT].async_connect()
|
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:
|
async def async_publish_service(call: ServiceCall) -> None:
|
||||||
"""Handle MQTT publish service calls."""
|
"""Handle MQTT publish service calls."""
|
||||||
msg_topic = call.data.get(ATTR_TOPIC)
|
msg_topic = call.data.get(ATTR_TOPIC)
|
||||||
|
@ -375,7 +387,6 @@ async def async_setup_entry( # noqa: C901
|
||||||
)
|
)
|
||||||
|
|
||||||
# setup platforms and discovery
|
# setup platforms and discovery
|
||||||
hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
|
|
||||||
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
|
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
|
||||||
|
|
||||||
async def async_setup_reload_service() -> None:
|
async def async_setup_reload_service() -> None:
|
||||||
|
@ -411,6 +422,7 @@ async def async_setup_entry( # noqa: C901
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
from . import device_automation, tag
|
from . import device_automation, tag
|
||||||
|
|
||||||
|
# Forward the entry setup to the MQTT platforms
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
[
|
[
|
||||||
|
@ -428,21 +440,25 @@ async def async_setup_entry( # noqa: C901
|
||||||
await _async_setup_discovery(hass, conf, entry)
|
await _async_setup_discovery(hass, conf, entry)
|
||||||
# Setup reload service after all platforms have loaded
|
# Setup reload service after all platforms have loaded
|
||||||
await async_setup_reload_service()
|
await async_setup_reload_service()
|
||||||
|
|
||||||
if DATA_MQTT_RELOAD_NEEDED in hass.data:
|
if DATA_MQTT_RELOAD_NEEDED in hass.data:
|
||||||
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
|
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
|
||||||
await hass.services.async_call(
|
await async_reload_manual_mqtt_items(hass)
|
||||||
DOMAIN,
|
|
||||||
SERVICE_RELOAD,
|
|
||||||
{},
|
|
||||||
blocking=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await async_forward_entry_setup_and_setup_discovery(entry)
|
await async_forward_entry_setup_and_setup_discovery(entry)
|
||||||
|
|
||||||
return True
|
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(
|
@websocket_api.websocket_command(
|
||||||
{vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str}
|
{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)
|
await device_automation.async_removed_from_device(hass, device_entry.id)
|
||||||
return True
|
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
|
||||||
|
|
|
@ -156,8 +156,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT Alarm Control Panel platform."""
|
||||||
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -112,8 +112,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT binary sensor."""
|
||||||
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -91,8 +91,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT button."""
|
||||||
async_add_entities([MqttButton(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttButton(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -89,8 +89,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT Camera."""
|
||||||
async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,9 @@ from homeassistant.const import (
|
||||||
CONF_PROTOCOL,
|
CONF_PROTOCOL,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
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.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
@ -59,6 +60,7 @@ from .models import (
|
||||||
ReceiveMessage,
|
ReceiveMessage,
|
||||||
ReceivePayloadType,
|
ReceivePayloadType,
|
||||||
)
|
)
|
||||||
|
from .util import mqtt_config_entry_enabled
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Only import for paho-mqtt type checking here, imports are done locally
|
# Only import for paho-mqtt type checking here, imports are done locally
|
||||||
|
@ -95,6 +97,10 @@ async def async_publish(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Publish message to a MQTT topic."""
|
"""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
|
outgoing_payload = payload
|
||||||
if not isinstance(payload, bytes):
|
if not isinstance(payload, bytes):
|
||||||
if not encoding:
|
if not encoding:
|
||||||
|
@ -174,6 +180,10 @@ async def async_subscribe(
|
||||||
|
|
||||||
Call the return value to unsubscribe.
|
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
|
# Count callback parameters which don't have a default value
|
||||||
non_default = 0
|
non_default = 0
|
||||||
if msg_callback:
|
if msg_callback:
|
||||||
|
@ -316,6 +326,8 @@ class MQTT:
|
||||||
self._last_subscribe = time.time()
|
self._last_subscribe = time.time()
|
||||||
self._mqttc: mqtt.Client = None
|
self._mqttc: mqtt.Client = None
|
||||||
self._paho_lock = asyncio.Lock()
|
self._paho_lock = asyncio.Lock()
|
||||||
|
self._pending_acks: set[int] = set()
|
||||||
|
self._cleanup_on_unload: list[Callable] = []
|
||||||
|
|
||||||
self._pending_operations: dict[str, asyncio.Event] = {}
|
self._pending_operations: dict[str, asyncio.Event] = {}
|
||||||
|
|
||||||
|
@ -331,6 +343,20 @@ class MQTT:
|
||||||
|
|
||||||
self.init_client()
|
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):
|
def init_client(self):
|
||||||
"""Initialize paho client."""
|
"""Initialize paho client."""
|
||||||
self._mqttc = MqttClientSetup(self.conf).client
|
self._mqttc = MqttClientSetup(self.conf).client
|
||||||
|
@ -405,6 +431,15 @@ class MQTT:
|
||||||
# Do not disconnect, we want the broker to always publish will
|
# Do not disconnect, we want the broker to always publish will
|
||||||
self._mqttc.loop_stop()
|
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)
|
await self.hass.async_add_executor_job(stop)
|
||||||
|
|
||||||
async def async_subscribe(
|
async def async_subscribe(
|
||||||
|
@ -440,7 +475,7 @@ class MQTT:
|
||||||
self.subscriptions.remove(subscription)
|
self.subscriptions.remove(subscription)
|
||||||
self._matching_subscriptions.cache_clear()
|
self._matching_subscriptions.cache_clear()
|
||||||
|
|
||||||
# Only unsubscribe if currently connected.
|
# Only unsubscribe if currently connected
|
||||||
if self.connected:
|
if self.connected:
|
||||||
self.hass.async_create_task(self._async_unsubscribe(topic))
|
self.hass.async_create_task(self._async_unsubscribe(topic))
|
||||||
|
|
||||||
|
@ -451,18 +486,20 @@ class MQTT:
|
||||||
|
|
||||||
This method is a coroutine.
|
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):
|
if any(other.topic == topic for other in self.subscriptions):
|
||||||
# Other subscriptions on topic remaining - don't unsubscribe.
|
# Other subscriptions on topic remaining - don't unsubscribe.
|
||||||
return
|
return
|
||||||
|
|
||||||
async with self._paho_lock:
|
async with self._paho_lock:
|
||||||
result: int | None = None
|
await self.hass.async_add_executor_job(_client_unsubscribe, topic)
|
||||||
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)
|
|
||||||
|
|
||||||
async def _async_perform_subscriptions(
|
async def _async_perform_subscriptions(
|
||||||
self, subscriptions: Iterable[tuple[str, int]]
|
self, subscriptions: Iterable[tuple[str, int]]
|
||||||
|
@ -643,6 +680,10 @@ class MQTT:
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
del self._pending_operations[mid]
|
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):
|
async def _discovery_cooldown(self):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
|
@ -401,8 +401,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT climate devices."""
|
||||||
async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,11 @@ CONF_TLS_INSECURE = "tls_insecure"
|
||||||
CONF_TLS_VERSION = "tls_version"
|
CONF_TLS_VERSION = "tls_version"
|
||||||
|
|
||||||
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
||||||
DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock"
|
|
||||||
DATA_MQTT = "mqtt"
|
DATA_MQTT = "mqtt"
|
||||||
DATA_MQTT_CONFIG = "mqtt_config"
|
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_RELOAD_NEEDED = "mqtt_reload_needed"
|
||||||
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"
|
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"
|
||||||
|
|
||||||
|
|
|
@ -251,8 +251,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT Cover."""
|
||||||
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import device_tracker
|
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 ..mixins import warn_for_legacy_schema
|
||||||
from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401
|
from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401
|
||||||
from .schema_discovery import async_setup_entry_from_discovery
|
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 = vol.All(
|
||||||
PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN)
|
PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Legacy setup
|
||||||
async_setup_scanner = async_setup_scanner_from_yaml
|
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]
|
||||||
|
)
|
||||||
|
|
|
@ -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 asyncio
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
@ -7,6 +9,7 @@ import voluptuous as vol
|
||||||
from homeassistant.components import device_tracker
|
from homeassistant.components import device_tracker
|
||||||
from homeassistant.components.device_tracker import SOURCE_TYPES
|
from homeassistant.components.device_tracker import SOURCE_TYPES
|
||||||
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_GPS_ACCURACY,
|
ATTR_GPS_ACCURACY,
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
|
@ -16,8 +19,10 @@ from homeassistant.const import (
|
||||||
STATE_HOME,
|
STATE_HOME,
|
||||||
STATE_NOT_HOME,
|
STATE_NOT_HOME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .. import subscription
|
from .. import subscription
|
||||||
from ..config import MQTT_RO_SCHEMA
|
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)
|
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."""
|
"""Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery."""
|
||||||
# load and initialize platform config from configuration.yaml
|
# load and initialize platform config from configuration.yaml
|
||||||
await asyncio.gather(
|
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(
|
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."""
|
"""Set up the MQTT Device Tracker entity."""
|
||||||
async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
"""Support for tracking MQTT enabled devices defined in YAML."""
|
"""Support for tracking MQTT enabled devices defined in YAML."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES
|
from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES
|
||||||
from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME
|
from homeassistant.const import CONF_DEVICES, 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 import config_validation as cv
|
||||||
|
|
||||||
|
from ... import mqtt
|
||||||
from ..client import async_subscribe
|
from ..client import async_subscribe
|
||||||
from ..config import SCHEMA_BASE
|
from ..config import SCHEMA_BASE
|
||||||
from ..const import CONF_QOS
|
from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY
|
||||||
from ..util import valid_subscribe_topic
|
from ..util import mqtt_config_entry_enabled, valid_subscribe_topic
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_PAYLOAD_HOME = "payload_home"
|
CONF_PAYLOAD_HOME = "payload_home"
|
||||||
CONF_PAYLOAD_NOT_HOME = "payload_not_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."""
|
"""Set up the MQTT tracker."""
|
||||||
devices = config[CONF_DEVICES]
|
devices = config[CONF_DEVICES]
|
||||||
qos = config[CONF_QOS]
|
qos = config[CONF_QOS]
|
||||||
payload_home = config[CONF_PAYLOAD_HOME]
|
payload_home = config[CONF_PAYLOAD_HOME]
|
||||||
payload_not_home = config[CONF_PAYLOAD_NOT_HOME]
|
payload_not_home = config[CONF_PAYLOAD_NOT_HOME]
|
||||||
source_type = config.get(CONF_SOURCE_TYPE)
|
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():
|
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))
|
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
|
return True
|
||||||
|
|
|
@ -234,8 +234,7 @@ async def async_start( # noqa: C901
|
||||||
hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None
|
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[ALREADY_DISCOVERED] = {}
|
||||||
hass.data[PENDING_DISCOVERED] = {}
|
hass.data[PENDING_DISCOVERED] = {}
|
||||||
|
|
||||||
|
|
|
@ -241,8 +241,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT fan."""
|
||||||
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -197,8 +197,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT humidifier."""
|
||||||
async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Support for MQTT lights."""
|
"""Support for MQTT lights."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -120,10 +121,14 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up a MQTT Light."""
|
||||||
setup_entity = {
|
setup_entity: dict[str, Callable] = {
|
||||||
"basic": async_setup_entity_basic,
|
"basic": async_setup_entity_basic,
|
||||||
"json": async_setup_entity_json,
|
"json": async_setup_entity_json,
|
||||||
"template": async_setup_entity_template,
|
"template": async_setup_entity_template,
|
||||||
|
|
|
@ -112,8 +112,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT Lock platform."""
|
||||||
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Coroutine
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Protocol, cast, final
|
from typing import Any, Protocol, cast, final
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ from .const import (
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
DATA_MQTT,
|
DATA_MQTT,
|
||||||
DATA_MQTT_CONFIG,
|
DATA_MQTT_CONFIG,
|
||||||
DATA_MQTT_RELOAD_NEEDED,
|
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||||
DATA_MQTT_UPDATED_CONFIG,
|
DATA_MQTT_UPDATED_CONFIG,
|
||||||
DEFAULT_ENCODING,
|
DEFAULT_ENCODING,
|
||||||
DEFAULT_PAYLOAD_AVAILABLE,
|
DEFAULT_PAYLOAD_AVAILABLE,
|
||||||
|
@ -84,7 +85,7 @@ from .subscription import (
|
||||||
async_subscribe_topics,
|
async_subscribe_topics,
|
||||||
async_unsubscribe_topics,
|
async_unsubscribe_topics,
|
||||||
)
|
)
|
||||||
from .util import valid_subscribe_topic
|
from .util import mqtt_config_entry_enabled, valid_subscribe_topic
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -299,11 +300,24 @@ async def async_get_platform_config_from_yaml(
|
||||||
return platform_configs
|
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."""
|
"""Set up entity, automation or tag creation dynamically through MQTT discovery."""
|
||||||
|
|
||||||
async def async_discover(discovery_payload):
|
async def async_discover(discovery_payload):
|
||||||
"""Discover and add an MQTT entity, automation or tag."""
|
"""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
|
discovery_data = discovery_payload.discovery_data
|
||||||
try:
|
try:
|
||||||
config = schema(discovery_payload)
|
config = schema(discovery_payload)
|
||||||
|
@ -316,8 +330,10 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema):
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async_dispatcher_connect(
|
hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append(
|
||||||
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
|
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_add_entities: AddEntitiesCallback,
|
||||||
async_setup_entities: SetupEntity,
|
async_setup_entities: SetupEntity,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Return true if platform setup should be aborted."""
|
"""Help to set up the platform for manual configured MQTT entities."""
|
||||||
if not bool(hass.config_entries.async_entries(DOMAIN)):
|
if not (entry_status := mqtt_config_entry_enabled(hass)):
|
||||||
hass.data[DATA_MQTT_RELOAD_NEEDED] = None
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"MQTT integration is not setup, skipping setup of manually configured "
|
"MQTT integration is %s, skipping setup of manually configured MQTT %s",
|
||||||
"MQTT %s",
|
"not setup" if entry_status is None else "disabled",
|
||||||
platform_domain,
|
platform_domain,
|
||||||
)
|
)
|
||||||
return
|
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):
|
def init_entity_id_from_config(hass, entity, config, entity_id_format):
|
||||||
|
@ -640,6 +657,7 @@ class MqttDiscoveryDeviceUpdate:
|
||||||
MQTT_DISCOVERY_UPDATED.format(discovery_hash),
|
MQTT_DISCOVERY_UPDATED.format(discovery_hash),
|
||||||
self.async_discovery_update,
|
self.async_discovery_update,
|
||||||
)
|
)
|
||||||
|
config_entry.async_on_unload(self._entry_unload)
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
self._remove_device_updated = hass.bus.async_listen(
|
self._remove_device_updated = hass.bus.async_listen(
|
||||||
EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed
|
EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed
|
||||||
|
@ -650,6 +668,14 @@ class MqttDiscoveryDeviceUpdate:
|
||||||
discovery_hash,
|
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(
|
async def async_discovery_update(
|
||||||
self,
|
self,
|
||||||
discovery_payload: DiscoveryInfoType | None,
|
discovery_payload: DiscoveryInfoType | None,
|
||||||
|
@ -734,7 +760,11 @@ class MqttDiscoveryDeviceUpdate:
|
||||||
class MqttDiscoveryUpdate(Entity):
|
class MqttDiscoveryUpdate(Entity):
|
||||||
"""Mixin used to handle updated discovery message for entity based platforms."""
|
"""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."""
|
"""Initialize the discovery update mixin."""
|
||||||
self._discovery_data = discovery_data
|
self._discovery_data = discovery_data
|
||||||
self._discovery_update = discovery_update
|
self._discovery_update = discovery_update
|
||||||
|
|
|
@ -145,8 +145,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT number."""
|
||||||
async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT scene."""
|
||||||
async_add_entities([MqttScene(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttScene(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -103,8 +103,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT select."""
|
||||||
async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -156,8 +156,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up MQTT sensor."""
|
||||||
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -152,8 +152,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT siren."""
|
||||||
async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -111,8 +111,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""Set up the MQTT switch."""
|
||||||
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])
|
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
"""Utility functions for the MQTT integration."""
|
"""Utility functions for the MQTT integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_PAYLOAD
|
from homeassistant.const import CONF_PAYLOAD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, template
|
from homeassistant.helpers import config_validation as cv, template
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -13,9 +17,17 @@ from .const import (
|
||||||
ATTR_TOPIC,
|
ATTR_TOPIC,
|
||||||
DEFAULT_QOS,
|
DEFAULT_QOS,
|
||||||
DEFAULT_RETAIN,
|
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:
|
def valid_topic(value: Any) -> str:
|
||||||
"""Validate that this is a valid topic name/filter."""
|
"""Validate that this is a valid topic name/filter."""
|
||||||
value = cv.string(value)
|
value = cv.string(value)
|
||||||
|
|
|
@ -100,10 +100,17 @@ async def async_setup_entry(
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_entity(
|
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."""
|
"""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]](
|
await setup_entity[config[CONF_SCHEMA]](
|
||||||
hass, config, async_add_entities, config_entry, discovery_data
|
hass, config, async_add_entities, config_entry, discovery_data
|
||||||
)
|
)
|
||||||
|
|
|
@ -60,6 +60,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -971,3 +972,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -44,6 +44,7 @@ from .test_common import (
|
||||||
help_test_setting_attribute_with_template,
|
help_test_setting_attribute_with_template,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1079,3 +1080,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -482,3 +483,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -36,6 +36,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -346,3 +347,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -55,6 +55,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1881,3 +1882,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import copy
|
import copy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from unittest.mock import ANY, patch
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.components import mqtt
|
||||||
from homeassistant.components.mqtt import debug_info
|
from homeassistant.components.mqtt import debug_info
|
||||||
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
|
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
|
||||||
from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED
|
from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
ATTR_ENTITY_ID,
|
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
|
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(
|
async def help_test_reloadable(
|
||||||
hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config
|
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],
|
domain: [new_config_1, new_config_2, new_config_3],
|
||||||
}
|
}
|
||||||
await help_test_reload_with_config(hass, caplog, tmp_path, new_config)
|
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
|
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):
|
async def help_test_setup_manual_entity_from_yaml(hass, platform, config):
|
||||||
"""Help to test setup from yaml through configuration entry."""
|
"""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}}
|
config_structure = {mqtt.DOMAIN: {platform: config}}
|
||||||
|
|
||||||
await async_setup_component(hass, mqtt.DOMAIN, config_structure)
|
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 = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
||||||
entry.add_to_hass(hass)
|
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
|
mock_client().connect = lambda *args: 0
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
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
|
||||||
|
|
|
@ -145,12 +145,17 @@ async def test_manual_config_starts_discovery_flow(
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_config_set(
|
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."""
|
"""Test manual config does not create an entry, and entry can be setup late."""
|
||||||
# MQTT config present in yaml config
|
# MQTT config present in yaml config
|
||||||
assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}})
|
assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}})
|
||||||
await hass.async_block_till_done()
|
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
|
assert len(mock_finish_setup.mock_calls) == 0
|
||||||
|
|
||||||
mock_try_connection.return_value = True
|
mock_try_connection.return_value = True
|
||||||
|
|
|
@ -73,6 +73,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -3364,3 +3365,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
"""The tests for the MQTT device tracker platform using configuration.yaml."""
|
"""The tests for the MQTT device tracker platform using configuration.yaml."""
|
||||||
|
import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH
|
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.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform
|
||||||
from homeassistant.setup import async_setup_component
|
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
|
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)
|
await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config)
|
||||||
|
|
||||||
assert hass.states.get(entity_id) is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .test_common import help_test_unload_config_entry
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
assert_lists_same,
|
assert_lists_same,
|
||||||
async_fire_mqtt_message,
|
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"
|
== "homeassistant/device_automation/bla2/config"
|
||||||
)
|
)
|
||||||
assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2
|
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
|
||||||
|
|
|
@ -58,6 +58,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1910,3 +1911,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -60,6 +60,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1296,3 +1297,12 @@ async def test_config_schema_validation(hass):
|
||||||
CONFIG_SCHEMA({DOMAIN: {platform: [config]}})
|
CONFIG_SCHEMA({DOMAIN: {platform: [config]}})
|
||||||
with pytest.raises(MultipleInvalid):
|
with pytest.raises(MultipleInvalid):
|
||||||
CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}})
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -17,6 +17,7 @@ from homeassistant.components import mqtt
|
||||||
from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info
|
from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info
|
||||||
from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA
|
from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA
|
||||||
from homeassistant.components.mqtt.models import ReceiveMessage
|
from homeassistant.components.mqtt.models import ReceiveMessage
|
||||||
|
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
|
@ -32,7 +33,10 @@ from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util.dt import utcnow
|
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 (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
|
@ -106,6 +110,18 @@ def record_calls(calls):
|
||||||
return record_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(
|
async def test_mqtt_connects_on_home_assistant_mqtt_setup(
|
||||||
hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config
|
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(
|
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."""
|
"""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)
|
hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
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):
|
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]
|
assert not mqtt_mock.async_publish.call_args[0][3]
|
||||||
|
|
||||||
|
|
||||||
async def test_publish_function_with_bad_encoding_conditions(hass, caplog):
|
async def test_publish_function_with_bad_encoding_conditions(
|
||||||
"""Test internal publish function with bas use cases."""
|
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(
|
await mqtt.async_publish(
|
||||||
hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None
|
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
|
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."""
|
"""Test for setup failure if initial client connection fails."""
|
||||||
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
mqtt_client_mock.connect.return_value = 1
|
mqtt_client_mock.connect.return_value = 1
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
try:
|
||||||
await hass.async_block_till_done()
|
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
|
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):
|
async def test_publish_error(hass, caplog):
|
||||||
"""Test publish error."""
|
"""Test publish error."""
|
||||||
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
# simulate an Out of memory error
|
# simulate an Out of memory error
|
||||||
with patch("paho.mqtt.client.Client") as mock_client:
|
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,
|
domain=mqtt.DOMAIN,
|
||||||
data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"},
|
data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"},
|
||||||
)
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch("paho.mqtt.client.Client") as mock_client:
|
with patch("paho.mqtt.client.Client") as mock_client:
|
||||||
mock_client().username_pw_set = mock_usename_password_set
|
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",
|
mqtt.config_integration.CONF_PROTOCOL: "3.1",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
with patch("paho.mqtt.client.Client") as mock_client:
|
with patch("paho.mqtt.client.Client") as mock_client:
|
||||||
mock_client.on_connect(return_value=0)
|
mock_client.on_connect(return_value=0)
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# check if protocol setup was correctly
|
# check if protocol setup was correctly
|
||||||
assert mock_client.call_args[1]["protocol"] == 3
|
assert mock_client.call_args[1]["protocol"] == 3
|
||||||
|
@ -1467,15 +1495,18 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog):
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}
|
domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}
|
||||||
)
|
)
|
||||||
# Set up the integration
|
entry.add_to_hass(hass)
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
|
||||||
# Make sure we are connected correctly
|
# Make sure we are connected correctly
|
||||||
mock_client.on_connect(mock_client, None, None, 0)
|
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
|
# Now call we publish without simulating and ACK callback
|
||||||
await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload")
|
await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload")
|
||||||
await hass.async_block_till_done()
|
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 len(mock_client.publish.mock_calls) == 1
|
||||||
assert "No ACK from MQTT server" in caplog.text
|
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):
|
async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog):
|
||||||
"""Test for setup failure if connection to broker is missing."""
|
"""Test for setup failure if connection to broker is missing."""
|
||||||
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
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("paho.mqtt.client.Client") as mock_client:
|
||||||
mock_client().connect = MagicMock(side_effect=OSError("Connection error"))
|
mock_client().connect = MagicMock(side_effect=OSError("Connection error"))
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
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
|
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,
|
domain=mqtt.DOMAIN,
|
||||||
data=config_item_data,
|
data=config_item_data,
|
||||||
)
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert calls
|
assert calls
|
||||||
|
|
||||||
|
@ -1546,8 +1580,9 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
|
||||||
domain=mqtt.DOMAIN,
|
domain=mqtt.DOMAIN,
|
||||||
data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"},
|
data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"},
|
||||||
)
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
assert await mqtt.async_setup_entry(hass, entry)
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert calls
|
assert calls
|
||||||
|
|
||||||
|
@ -2644,3 +2679,206 @@ async def test_config_schema_validation(hass):
|
||||||
config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}}
|
config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}}
|
||||||
with pytest.raises(vol.MultipleInvalid):
|
with pytest.raises(vol.MultipleInvalid):
|
||||||
CONFIG_SCHEMA(config)
|
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
|
||||||
|
|
|
@ -240,6 +240,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -3803,3 +3804,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -72,6 +72,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1266,3 +1267,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -48,6 +48,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
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"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -57,6 +57,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -853,3 +854,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from .test_common import (
|
||||||
help_test_reloadable_late,
|
help_test_reloadable_late,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
help_test_unique_id,
|
||||||
|
help_test_unload_config_entry_with_platform,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
|
@ -237,3 +238,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -48,6 +48,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -687,3 +688,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -58,6 +58,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -1213,3 +1214,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -45,6 +45,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -975,3 +976,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -42,6 +42,7 @@ from .test_common import (
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
help_test_setup_manual_entity_from_yaml,
|
help_test_setup_manual_entity_from_yaml,
|
||||||
help_test_unique_id,
|
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_bad_JSON,
|
||||||
help_test_update_with_json_attrs_not_dict,
|
help_test_update_with_json_attrs_not_dict,
|
||||||
)
|
)
|
||||||
|
@ -664,3 +665,12 @@ async def test_setup_manual_entity_from_yaml(hass):
|
||||||
del config["platform"]
|
del config["platform"]
|
||||||
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
await help_test_setup_manual_entity_from_yaml(hass, platform, config)
|
||||||
assert hass.states.get(f"{platform}.test") is not None
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -11,6 +11,8 @@ from homeassistant.const import Platform
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .test_common import help_test_unload_config_entry
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
async_fire_mqtt_message,
|
async_fire_mqtt_message,
|
||||||
|
@ -797,3 +799,28 @@ async def test_cleanup_device_with_entity2(
|
||||||
# Verify device registry entry is cleared
|
# Verify device registry entry is cleared
|
||||||
device_entry = device_reg.async_get_device({("mqtt", "helloworld")})
|
device_entry = device_reg.async_get_device({("mqtt", "helloworld")})
|
||||||
assert device_entry is None
|
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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue