diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index c245b66fdb1..165d273f46e 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -29,7 +29,6 @@ from .const import ( CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON, PAYLOAD_NONE, - TEMPLATE_ERRORS, ) from .debug_info import log_messages from .mixins import ( @@ -39,6 +38,7 @@ from .mixins import ( ) from .models import ( MqttValueTemplate, + MqttValueTemplateException, PayloadSentinel, ReceiveMessage, ReceivePayloadType, @@ -134,7 +134,8 @@ class MqttEvent(MqttEntity, EventEntity): event_type: str try: payload = self._template(msg.payload, PayloadSentinel.DEFAULT) - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if ( not payload diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index e91a8c5c259..91e11d06371 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -24,14 +24,19 @@ from homeassistant.util import dt as dt_util from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, TEMPLATE_ERRORS +from .const import CONF_ENCODING, CONF_QOS from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entity_entry_helper, ) -from .models import MessageCallbackType, MqttValueTemplate, ReceiveMessage +from .models import ( + MessageCallbackType, + MqttValueTemplate, + MqttValueTemplateException, + ReceiveMessage, +) from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -191,7 +196,8 @@ class MqttImage(MqttEntity, ImageEntity): try: url = cv.url(self._url_template(msg.payload)) self._attr_image_url = url - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return except vol.Invalid: _LOGGER.error( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 5736f821f69..554e83204dd 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -94,7 +94,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - TEMPLATE_ERRORS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -109,6 +108,7 @@ from .discovery import ( from .models import ( MessageCallbackType, MqttValueTemplate, + MqttValueTemplateException, PublishPayloadType, ReceiveMessage, ) @@ -482,7 +482,8 @@ def write_state_on_attr_change( } try: msg_callback(msg) - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if not _attrs_have_changed(tracked_attrs): return diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 1295bfb8ff3..9fcea353299 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -223,6 +223,36 @@ class MqttCommandTemplate: ) from exc +class MqttValueTemplateException(TemplateError): + """Handle MqttValueTemplate exceptions.""" + + def __init__( + self, + *args: object, + base_exception: Exception, + value_template: str, + default: ReceivePayloadType | PayloadSentinel, + payload: ReceivePayloadType, + entity_id: str | None = None, + ) -> None: + """Initialize exception.""" + super().__init__(base_exception, *args) + entity_id_log = "" if entity_id is None else f" for entity '{entity_id}'" + default_log = str(default) + default_payload_log = ( + "" if default is PayloadSentinel.NONE else f", default value: {default_log}" + ) + payload_log = str(payload) + self._message = ( + f"{type(base_exception).__name__}: {base_exception} rendering template{entity_id_log}" + f", template: '{value_template}'{default_payload_log} and payload: {payload_log}" + ) + + def __str__(self) -> str: + """Return exception message string.""" + return self._message + + class MqttValueTemplate: """Class for rendering MQTT value template with possible json values.""" @@ -291,14 +321,13 @@ class MqttValueTemplate: ) ) except TEMPLATE_ERRORS as exc: - _LOGGER.error( - "%s: %s rendering template for entity '%s', template: '%s'", - type(exc).__name__, - exc, - self._entity.entity_id if self._entity else "n/a", - self._value_template.template, - ) - raise + raise MqttValueTemplateException( + base_exception=exc, + value_template=self._value_template.template, + default=default, + payload=payload, + entity_id=self._entity.entity_id if self._entity else None, + ) from exc return rendered_payload _LOGGER.debug( @@ -318,17 +347,13 @@ class MqttValueTemplate: ) ) except TEMPLATE_ERRORS as exc: - _LOGGER.error( - "%s: %s rendering template for entity '%s', template: " - "'%s', default value: %s and payload: %s", - type(exc).__name__, - exc, - self._entity.entity_id if self._entity else "n/a", - self._value_template.template, - default, - payload, - ) - raise + raise MqttValueTemplateException( + base_exception=exc, + value_template=self._value_template.template, + default=default, + payload=payload, + entity_id=self._entity.entity_id if self._entity else None, + ) from exc return rendered_payload diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 0eda584e95a..42c8760c302 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable import functools +import logging import voluptuous as vol @@ -15,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC, TEMPLATE_ERRORS +from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC from .discovery import MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -25,10 +26,17 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType +from .models import ( + MqttValueTemplate, + MqttValueTemplateException, + ReceiveMessage, + ReceivePayloadType, +) from .subscription import EntitySubscription from .util import get_mqtt_data, valid_subscribe_topic +_LOGGER = logging.getLogger(__name__) + LOG_NAME = "Tag" TAG = "tag" @@ -138,7 +146,8 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): async def tag_scanned(msg: ReceiveMessage) -> None: try: tag_id = str(self._value_template(msg.payload, "")).strip() - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if not tag_id: # No output from template, ignore return diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9fe394bd797..16fa06ccd27 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -19,6 +19,7 @@ from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, + MqttValueTemplateException, ReceiveMessage, ) from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState @@ -433,37 +434,30 @@ async def test_value_template_value(hass: HomeAssistant) -> None: assert template_state_calls.call_count == 1 -async def test_value_template_fails( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_value_template_fails(hass: HomeAssistant) -> None: """Test the rendering of MQTT value template fails.""" - - # test rendering a value fails entity = MockEntity(entity_id="sensor.test") entity.hass = hass tpl = template.Template("{{ value_json.some_var * 2 }}") val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) - with pytest.raises(TypeError) as exc: + with pytest.raises(MqttValueTemplateException) as exc: val_tpl.async_render_with_possible_json_value('{"some_var": null }') - assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'" - assert ( + assert str(exc.value) == ( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "rendering template for entity 'sensor.test', " - "template: '{{ value_json.some_var * 2 }}'" - ) in caplog.text - caplog.clear() - with pytest.raises(TypeError) as exc: + "template: '{{ value_json.some_var * 2 }}' " + 'and payload: {"some_var": null }' + ) + with pytest.raises(MqttValueTemplateException) as exc: val_tpl.async_render_with_possible_json_value( '{"some_var": null }', default=100 ) - assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'" - assert ( + assert str(exc.value) == ( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "rendering template for entity 'sensor.test', " "template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: " '{"some_var": null }' - ) in caplog.text - await hass.async_block_till_done() + ) async def test_service_call_without_topic_does_not_publish(