From 5afe8fd2db0a6b4cb07ea717487badca5f01420a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 22 Mar 2022 12:51:24 +0100 Subject: [PATCH] Revert "Add MQTT notify platform (#64728)" (#68505) This reverts commit e574a3ef1d855304b2a78c389861c421b1548d74. --- homeassistant/components/mqtt/__init__.py | 1 - .../components/mqtt/abbreviations.py | 2 - homeassistant/components/mqtt/const.py | 12 +- homeassistant/components/mqtt/discovery.py | 12 +- homeassistant/components/mqtt/mixins.py | 19 +- homeassistant/components/mqtt/notify.py | 406 -------- tests/components/mqtt/test_notify.py | 863 ------------------ 7 files changed, 14 insertions(+), 1301 deletions(-) delete mode 100644 homeassistant/components/mqtt/notify.py delete mode 100644 tests/components/mqtt/test_notify.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c74417ece37..48da8fd7e73 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -160,7 +160,6 @@ PLATFORMS = [ Platform.HUMIDIFIER, Platform.LIGHT, Platform.LOCK, - Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SCENE, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 587f9617124..ddbced5286d 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -185,8 +185,6 @@ ABBREVIATIONS = { "set_fan_spd_t": "set_fan_speed_topic", "set_pos_tpl": "set_position_template", "set_pos_t": "set_position_topic", - "title": "title", - "trgts": "targets", "pos_t": "position_topic", "pos_tpl": "position_template", "spd_cmd_t": "speed_command_topic", diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 63b9d68b863..69865733763 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,6 +1,4 @@ """Constants used by multiple MQTT modules.""" -from typing import Final - from homeassistant.const import CONF_PAYLOAD ATTR_DISCOVERY_HASH = "discovery_hash" @@ -14,11 +12,11 @@ ATTR_TOPIC = "topic" CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" -CONF_COMMAND_TEMPLATE: Final = "command_template" -CONF_COMMAND_TOPIC: Final = "command_topic" -CONF_ENCODING: Final = "encoding" -CONF_QOS: Final = "qos" -CONF_RETAIN: Final = "retain" +CONF_COMMAND_TEMPLATE = "command_template" +CONF_COMMAND_TOPIC = "command_topic" +CONF_ENCODING = "encoding" +CONF_QOS = ATTR_QOS +CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 05e06fec666..11bc0f6839a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -15,7 +15,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -49,7 +48,6 @@ SUPPORTED_COMPONENTS = [ "humidifier", "light", "lock", - "notify", "number", "scene", "siren", @@ -234,15 +232,7 @@ async def async_start( # noqa: C901 from . import device_automation await device_automation.async_setup_entry(hass, config_entry) - elif component in "notify": - # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import notify - - await notify.async_setup_entry( - hass, config_entry, AddEntitiesCallback - ) - elif component in "tag": + elif component == "tag": # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import tag diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index c87e5ccba25..9f3722a8f31 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -5,7 +5,7 @@ from abc import abstractmethod from collections.abc import Callable import json import logging -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol @@ -237,10 +237,10 @@ class SetupEntity(Protocol): async def async_setup_entry_helper(hass, domain, async_setup, schema): - """Set up entity, automation, notify service or tag creation dynamically through MQTT discovery.""" + """Set up entity, automation or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): - """Discover and add an MQTT entity, automation, notify service or tag.""" + """Discover and add an MQTT entity, automation or tag.""" discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -496,13 +496,11 @@ class MqttAvailability(Entity): return self._available_latest -async def cleanup_device_registry( - hass: HomeAssistant, device_id: str | None, config_entry_id: str | None -) -> None: - """Remove device registry entry if there are no remaining entities, triggers or notify services.""" +async def cleanup_device_registry(hass, device_id, config_entry_id): + """Remove device registry entry if there are no remaining entities or triggers.""" # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import device_trigger, notify, tag + # pylint: disable-next=import-outside-toplevel + from . import device_trigger, tag device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -513,10 +511,9 @@ async def cleanup_device_registry( ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) - and not notify.device_has_notify_services(hass, device_id) ): device_registry.async_update_device( - device_id, remove_config_entry_id=cast(str, config_entry_id) + device_id, remove_config_entry_id=config_entry_id ) diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py deleted file mode 100644 index 9ba341aab0d..00000000000 --- a/homeassistant/components/mqtt/notify.py +++ /dev/null @@ -1,406 +0,0 @@ -"""Support for MQTT notify.""" -from __future__ import annotations - -import functools -import logging -from typing import Any, Final, TypedDict, cast - -import voluptuous as vol - -from homeassistant.components import notify -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify - -from . import PLATFORMS, MqttCommandTemplate -from .. import mqtt -from .const import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_PAYLOAD, - CONF_COMMAND_TEMPLATE, - CONF_COMMAND_TOPIC, - CONF_ENCODING, - CONF_QOS, - CONF_RETAIN, - DOMAIN, -) -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash -from .mixins import ( - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - async_setup_entry_helper, - cleanup_device_registry, - device_info_from_config, -) - -CONF_TARGETS: Final = "targets" -CONF_TITLE: Final = "title" -CONF_CONFIG_ENTRY: Final = "config_entry" -CONF_DISCOVER_HASH: Final = "discovery_hash" - -MQTT_NOTIFY_SERVICES_SETUP = "mqtt_notify_services_setup" - -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_TARGETS, default=[]): cv.ensure_list, - vol.Optional(CONF_TITLE, default=notify.ATTR_TITLE_DEFAULT): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - } -) - -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - }, - extra=vol.REMOVE_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -class MqttNotificationConfig(TypedDict, total=False): - """Supply service parameters for MqttNotificationService.""" - - command_topic: str - command_template: Template - encoding: str - name: str | None - qos: int - retain: bool - targets: list - title: str - device: ConfigType - - -async def async_initialize(hass: HomeAssistant) -> None: - """Initialize globals.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - hass.data.setdefault(MQTT_NOTIFY_SERVICES_SETUP, {}) - - -def device_has_notify_services(hass: HomeAssistant, device_id: str) -> bool: - """Check if the device has registered notify services.""" - if MQTT_NOTIFY_SERVICES_SETUP not in hass.data: - return False - for key, service in hass.data[ # pylint: disable=unused-variable - MQTT_NOTIFY_SERVICES_SETUP - ].items(): - if service.device_id == device_id: - return True - return False - - -def _check_notify_service_name( - hass: HomeAssistant, config: MqttNotificationConfig -) -> str | None: - """Check if the service already exists or else return the service name.""" - service_name = slugify(config[CONF_NAME]) - has_services = hass.services.has_service(notify.DOMAIN, service_name) - services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] - if service_name in services.keys() or has_services: - _LOGGER.error( - "Notify service '%s' already exists, cannot register service", - service_name, - ) - return None - return service_name - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up MQTT notify service dynamically through MQTT discovery.""" - await async_initialize(hass) - setup = functools.partial(_async_setup_notify, hass, config_entry=config_entry) - await async_setup_entry_helper(hass, notify.DOMAIN, setup, DISCOVERY_SCHEMA) - - -async def _async_setup_notify( - hass, - legacy_config: ConfigType, - config_entry: ConfigEntry, - discovery_data: dict[str, Any], -): - """Set up the MQTT notify service with auto discovery.""" - config: MqttNotificationConfig = DISCOVERY_SCHEMA( - discovery_data[ATTR_DISCOVERY_PAYLOAD] - ) - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - - if not (service_name := _check_notify_service_name(hass, config)): - async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) - clear_discovery_hash(hass, discovery_hash) - return - - device_id = _update_device(hass, config_entry, config) - - service = MqttNotificationService( - hass, - config, - config_entry, - device_id, - discovery_hash, - ) - hass.data[MQTT_NOTIFY_SERVICES_SETUP][service_name] = service - - await service.async_setup(hass, service_name, service_name) - await service.async_register_services() - - -async def async_get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> MqttNotificationService | None: - """Prepare the MQTT notification service through configuration.yaml.""" - await async_initialize(hass) - notification_config: MqttNotificationConfig = cast(MqttNotificationConfig, config) - - if not (service_name := _check_notify_service_name(hass, notification_config)): - return None - - service = hass.data[MQTT_NOTIFY_SERVICES_SETUP][ - service_name - ] = MqttNotificationService( - hass, - notification_config, - ) - return service - - -class MqttNotificationServiceUpdater: - """Add support for auto discovery updates.""" - - def __init__(self, hass: HomeAssistant, service: MqttNotificationService) -> None: - """Initialize the update service.""" - - async def async_discovery_update( - discovery_payload: DiscoveryInfoType | None, - ) -> None: - """Handle discovery update.""" - if not discovery_payload: - # unregister notify service through auto discovery - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - await async_tear_down_service() - return - - # update notify service through auto discovery - await service.async_update_service(discovery_payload) - _LOGGER.debug( - "Notify service %s updated has been processed", - service.discovery_hash, - ) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - - async def async_device_removed(event): - """Handle the removal of a device.""" - device_id = event.data["device_id"] - if ( - event.data["action"] != "remove" - or device_id != service.device_id - or self._device_removed - ): - return - self._device_removed = True - await async_tear_down_service() - - async def async_tear_down_service(): - """Handle the removal of the service.""" - services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] - if self._service.service_name in services.keys(): - del services[self._service.service_name] - if not self._device_removed and service.config_entry: - self._device_removed = True - await cleanup_device_registry( - hass, service.device_id, service.config_entry.entry_id - ) - clear_discovery_hash(hass, service.discovery_hash) - self._remove_discovery() - await service.async_unregister_services() - _LOGGER.info( - "Notify service %s has been removed", - service.discovery_hash, - ) - del self._service - - self._service = service - self._remove_discovery = async_dispatcher_connect( - hass, - MQTT_DISCOVERY_UPDATED.format(service.discovery_hash), - async_discovery_update, - ) - if service.device_id: - self._remove_device_updated = hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed - ) - self._device_removed = False - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - _LOGGER.info( - "Notify service %s has been initialized", - service.discovery_hash, - ) - - -class MqttNotificationService(notify.BaseNotificationService): - """Implement the notification service for MQTT.""" - - def __init__( - self, - hass: HomeAssistant, - service_config: MqttNotificationConfig, - config_entry: ConfigEntry | None = None, - device_id: str | None = None, - discovery_hash: tuple | None = None, - ) -> None: - """Initialize the service.""" - self.hass = hass - self._config = service_config - self._commmand_template = MqttCommandTemplate( - service_config.get(CONF_COMMAND_TEMPLATE), hass=hass - ) - self._device_id = device_id - self._discovery_hash = discovery_hash - self._config_entry = config_entry - self._service_name = slugify(service_config[CONF_NAME]) - - self._updater = ( - MqttNotificationServiceUpdater(hass, self) if discovery_hash else None - ) - - @property - def device_id(self) -> str | None: - """Return the device ID.""" - return self._device_id - - @property - def config_entry(self) -> ConfigEntry | None: - """Return the config_entry.""" - return self._config_entry - - @property - def discovery_hash(self) -> tuple | None: - """Return the discovery hash.""" - return self._discovery_hash - - @property - def service_name(self) -> str: - """Return the service ma,e.""" - return self._service_name - - async def async_update_service( - self, - discovery_payload: DiscoveryInfoType, - ) -> None: - """Update the notify service through auto discovery.""" - config: MqttNotificationConfig = DISCOVERY_SCHEMA(discovery_payload) - # Do not rename a service if that service_name is already in use - if ( - new_service_name := slugify(config[CONF_NAME]) - ) != self._service_name and _check_notify_service_name( - self.hass, config - ) is None: - return - # Only refresh services if service name or targets have changes - if ( - new_service_name != self._service_name - or config[CONF_TARGETS] != self._config[CONF_TARGETS] - ): - services = self.hass.data[MQTT_NOTIFY_SERVICES_SETUP] - await self.async_unregister_services() - if self._service_name in services: - del services[self._service_name] - self._config = config - self._service_name = new_service_name - await self.async_register_services() - services[new_service_name] = self - else: - self._config = config - self._commmand_template = MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), hass=self.hass - ) - _update_device(self.hass, self._config_entry, config) - - @property - def targets(self) -> dict[str, str]: - """Return a dictionary of registered targets.""" - return {target: target for target in self._config[CONF_TARGETS]} - - async def async_send_message(self, message: str = "", **kwargs): - """Build and send a MQTT message.""" - target = kwargs.get(notify.ATTR_TARGET) - if ( - target is not None - and self._config[CONF_TARGETS] - and set(target) & set(self._config[CONF_TARGETS]) != set(target) - ): - _LOGGER.error( - "Cannot send %s, target list %s is invalid, valid available targets: %s", - message, - target, - self._config[CONF_TARGETS], - ) - return - variables = { - "message": message, - "name": self._config[CONF_NAME], - "service": self._service_name, - "target": target or self._config[CONF_TARGETS], - "title": kwargs.get(notify.ATTR_TITLE, self._config[CONF_TITLE]), - } - variables.update(kwargs.get(notify.ATTR_DATA) or {}) - payload = self._commmand_template.async_render( - message, - variables=variables, - ) - await mqtt.async_publish( - self.hass, - self._config[CONF_COMMAND_TOPIC], - payload, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - -def _update_device( - hass: HomeAssistant, - config_entry: ConfigEntry | None, - config: MqttNotificationConfig, -) -> str | None: - """Update device registry.""" - if config_entry is None or CONF_DEVICE not in config: - return None - - device = None - device_registry = dr.async_get(hass) - config_entry_id = config_entry.entry_id - device_info = device_info_from_config(config[CONF_DEVICE]) - - if config_entry_id is not None and device_info is not None: - update_device_info = cast(dict, device_info) - update_device_info["config_entry_id"] = config_entry_id - device = device_registry.async_get_or_create(**update_device_info) - - return device.id if device else None diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py deleted file mode 100644 index 33a32d858af..00000000000 --- a/tests/components/mqtt/test_notify.py +++ /dev/null @@ -1,863 +0,0 @@ -"""The tests for the MQTT button platform.""" -import copy -import json -from unittest.mock import patch - -import pytest -import yaml - -from homeassistant import config as hass_config -from homeassistant.components import notify -from homeassistant.components.mqtt import DOMAIN -from homeassistant.const import CONF_NAME, SERVICE_RELOAD -from homeassistant.exceptions import ServiceNotFound -from homeassistant.setup import async_setup_component -from homeassistant.util import slugify - -from tests.common import async_fire_mqtt_message, mock_device_registry - -DEFAULT_CONFIG = {notify.DOMAIN: {"platform": "mqtt", "command_topic": "test-topic"}} - -COMMAND_TEMPLATE_TEST_PARAMS = ( - "name,service,parameters,expected_result", - [ - ( - "My service", - "my_service", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val1"}, - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val1",' - '"target":[' - "'t1', 't2'" - "]," - '"title":"Title"}', - ), - ( - "My service", - "my_service", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val1"}, - notify.ATTR_TARGET: ["t2"], - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val1",' - '"target":[' - "'t2'" - "]," - '"title":"Title"}', - ), - ( - "My service", - "my_service_t1", - { - notify.ATTR_TITLE: "Title2", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val2"}, - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val2",' - '"target":[' - "'t1'" - "]," - '"title":"Title2"}', - ), - ], -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -async def async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data, service_name -): - """Test setup notify service with a device config.""" - caplog.clear() - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/{service_name}/config", data - ) - await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is not None - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) -async def test_sending_with_command_templates_with_config_setup( - hass, mqtt_mock, caplog, name, service, parameters, expected_result -): - """Test the sending MQTT commands using a template using config setup.""" - config = { - "name": name, - "command_topic": "lcd/set", - "command_template": "{" - '"message":"{{message}}",' - '"name":"{{name}}",' - '"service":"{{service}}",' - '"par1":"{{par1}}",' - '"target":{{target}},' - '"title":"{{title}}"' - "}", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "1", - } - service_base_name = slugify(name) - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: config}, - ) - await hass.async_block_till_done() - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - service, - parameters, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "lcd/set", expected_result, 1, False - ) - mqtt_mock.async_publish.reset_mock() - - -@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) -async def test_sending_with_command_templates_auto_discovery( - hass, mqtt_mock, caplog, name, service, parameters, expected_result -): - """Test the sending MQTT commands using a template and auto discovery.""" - config = { - "name": name, - "command_topic": "lcd/set", - "command_template": "{" - '"message":"{{message}}",' - '"name":"{{name}}",' - '"service":"{{service}}",' - '"par1":"{{par1}}",' - '"target":{{target}},' - '"title":"{{title}}"' - "}", - "targets": ["t1", "t2"], - "qos": "1", - } - if name: - config[CONF_NAME] = name - service_base_name = slugify(name) - else: - service_base_name = DOMAIN - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", json.dumps(config) - ) - await hass.async_block_till_done() - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - service, - parameters, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "lcd/set", expected_result, 1, False - ) - mqtt_mock.async_publish.reset_mock() - - -async def test_sending_mqtt_commands(hass, mqtt_mock, caplog): - """Test the sending MQTT commands.""" - config1 = { - "command_topic": "command-topic1", - "name": "test1", - "platform": "mqtt", - "qos": "2", - } - config2 = { - "command_topic": "command-topic2", - "name": "test2", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1, config2]}, - ) - await hass.async_block_till_done() - assert "" in caplog.text - assert "" in caplog.text - assert ( - "" in caplog.text - ) - assert ( - "" in caplog.text - ) - - # test1 simple call without targets - await hass.services.async_call( - notify.DOMAIN, - "test1", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic1", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call without targets - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service without target - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with empty target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: [], - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with single target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t1"], - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with invalid target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["invalid"], - }, - blocking=True, - ) - - assert ( - "Cannot send Message, target list ['invalid'] is invalid, valid available targets: ['t1', 't2']" - in caplog.text - ) - mqtt_mock.async_publish.call_count == 0 - mqtt_mock.async_publish.reset_mock() - - -async def test_with_same_name(hass, mqtt_mock, caplog): - """Test the multiple setups with the same name.""" - config1 = { - "command_topic": "command-topic1", - "name": "test_same_name", - "platform": "mqtt", - "qos": "2", - } - config2 = { - "command_topic": "command-topic2", - "name": "test_same_name", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1, config2]}, - ) - await hass.async_block_till_done() - assert ( - "" - in caplog.text - ) - assert ( - "Notify service 'test_same_name' already exists, cannot register service" - in caplog.text - ) - - # test call main service on service with multiple targets with the same name - # the first configured service should publish - await hass.services.async_call( - notify.DOMAIN, - "test_same_name", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic1", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(ServiceNotFound): - await hass.services.async_call( - notify.DOMAIN, - "test_same_name_t2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t2"], - }, - blocking=True, - ) - - -async def test_discovery_without_device(hass, mqtt_mock, caplog): - """Test discovery, update and removal of notify service without device.""" - data = '{ "name": "Old name", "command_topic": "test_topic" }' - data_update = '{ "command_topic": "test_topic_update", "name": "New name" }' - data_update_with_targets1 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"] }' - data_update_with_targets2 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target3"] }' - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert ( - "" in caplog.text - ) - - await hass.services.async_call( - notify.DOMAIN, - "old_name", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with("test_topic", "Message", 0, False) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update - ) - await hass.async_block_till_done() - - assert "" in caplog.text - assert ( - "" in caplog.text - ) - assert "Notify service ('notify', 'bla') updated has been processed" in caplog.text - - await hass.services.async_call( - notify.DOMAIN, - "new_name", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "test_topic_update", "Message", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "" in caplog.text - - # rediscover with targets - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets1 - ) - await hass.async_block_till_done() - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # update available targets - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets2 - ) - await hass.async_block_till_done() - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # test if a new service with same name fails to setup - config1 = { - "command_topic": "command-topic-config.yaml", - "name": "test-setup1", - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1]}, - ) - await hass.async_block_till_done() - data = '{ "name": "test-setup1", "command_topic": "test_topic" }' - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/test-setup1/config", data - ) - await hass.async_block_till_done() - assert ( - "Notify service 'test_setup1' already exists, cannot register service" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - "test_setup1", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t2"], - }, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "command-topic-config.yaml", "Message", 2, False - ) - - # Test with same discovery on new name - data = '{ "name": "testa", "command_topic": "test_topic_a" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testa/config", data) - await hass.async_block_till_done() - assert "" in caplog.text - - data = '{ "name": "testb", "command_topic": "test_topic_b" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) - await hass.async_block_till_done() - assert "" in caplog.text - - # Try to update from new discovery of existing service test - data = '{ "name": "testa", "command_topic": "test_topic_c" }' - caplog.clear() - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testc/config", data) - await hass.async_block_till_done() - assert ( - "Notify service 'testa' already exists, cannot register service" in caplog.text - ) - - # Try to update the same discovery to existing service test - data = '{ "name": "testa", "command_topic": "test_topic_c" }' - caplog.clear() - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) - await hass.async_block_till_done() - assert ( - "Notify service 'testa' already exists, cannot register service" in caplog.text - ) - - -async def test_discovery_with_device_update(hass, mqtt_mock, caplog, device_reg): - """Test discovery, update and removal of notify service with a device config.""" - - # Initial setup - data = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - service_name = "my_notify_service" - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data, service_name - ) - assert "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -async def test_discovery_with_device_removal(hass, mqtt_mock, caplog, device_reg): - """Test discovery, update and removal of notify service with a device config.""" - - # Initial setup - data1 = '{ "command_topic": "test_topic", "name": "My notify service1", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - data2 = '{ "command_topic": "test_topic", "name": "My notify service2", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - service_name1 = "my_notify_service1" - service_name2 = "my_notify_service2" - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data1, service_name1 - ) - assert "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - not in caplog.text - ) - caplog.clear() - - # The device should still be there - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is not None - device_id = device_entry.id - assert device_id == device_entry.id - assert device_entry.name == "Test123" - - # Test removal device from device registry after removing second service - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/{service_name2}/config", "{}" - ) - await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is None - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - caplog.clear() - - # Recreate the service and device - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data1, service_name1 - ) - assert "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -async def test_publishing_with_custom_encoding(hass, mqtt_mock, caplog): - """Test publishing MQTT payload with different encoding via discovery and configuration.""" - # test with default encoding using configuration setup - assert await async_setup_component( - hass, - notify.DOMAIN, - { - notify.DOMAIN: { - "command_topic": "command-topic", - "name": "test", - "platform": "mqtt", - "qos": "2", - } - }, - ) - await hass.async_block_till_done() - - # test with raw encoding and discovery - data = '{"name": "test2", "command_topic": "test_topic2", "command_template": "{{ pack(int(message), \'b\') }}" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been initialized" in caplog.text - assert "" in caplog.text - - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "4"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with("test_topic2", b"\x04", 0, False) - mqtt_mock.async_publish.reset_mock() - - # test with utf-16 and update discovery - data = '{"encoding":"utf-16", "name": "test3", "command_topic": "test_topic3", "command_template": "{{ message }}" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - assert ( - "Component has already been discovered: notify bla, sending update" - in caplog.text - ) - - await hass.services.async_call( - notify.DOMAIN, - "test3", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "test_topic3", "Message".encode("utf-16"), 0, False - ) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been removed" in caplog.text - - -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): - """Test reloading the MQTT platform.""" - domain = notify.DOMAIN - config = DEFAULT_CONFIG[domain] - - # Create and test an old config of 2 entities based on the config supplied - old_config_1 = copy.deepcopy(config) - old_config_1["name"] = "Test old 1" - old_config_2 = copy.deepcopy(config) - old_config_2["name"] = "Test old 2" - - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) - await hass.async_block_till_done() - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # Add an auto discovered notify target - data = '{"name": "Test old 3", "command_topic": "test_topic_discovery" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been initialized" in caplog.text - assert ( - "" - in caplog.text - ) - - # Create temporary fixture for configuration.yaml based on the supplied config and test a reload with this new config - new_config_1 = copy.deepcopy(config) - new_config_1["name"] = "Test new 1" - new_config_2 = copy.deepcopy(config) - new_config_2["name"] = "test new 2" - new_config_3 = copy.deepcopy(config) - new_config_3["name"] = "test new 3" - new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: [new_config_1, new_config_2, new_config_3]}) - 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): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert ( - "" in caplog.text - ) - assert ( - "" in caplog.text - ) - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert "" in caplog.text - caplog.clear() - - # test if the auto discovered item survived the platform reload - await hass.services.async_call( - notify.DOMAIN, - "test_old_3", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "test_topic_discovery", "Message", 0, False - ) - - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been removed" in caplog.text