diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c8da1d7d8bc..4eadc0e98b3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -7,7 +7,6 @@ from datetime import datetime import logging from typing import TYPE_CHECKING, Any, TypeVar, cast -import jinja2 import voluptuous as vol from homeassistant import config as conf_util @@ -27,7 +26,6 @@ from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ( ConfigValidationError, ServiceValidationError, - TemplateError, Unauthorized, ) from homeassistant.helpers import config_validation as cv, event as ev, template @@ -87,11 +85,13 @@ from .const import ( # noqa: F401 MQTT_DISCONNECTED, PLATFORMS, RELOADABLE_PLATFORMS, + TEMPLATE_ERRORS, ) from .models import ( # noqa: F401 MqttCommandTemplate, MqttData, MqttValueTemplate, + PayloadSentinel, PublishPayloadType, ReceiveMessage, ReceivePayloadType, @@ -325,7 +325,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: msg_topic_template, hass ).async_render(parse_result=False) msg_topic = valid_publish_topic(rendered_topic) - except (jinja2.TemplateError, TemplateError) as exc: + except TEMPLATE_ERRORS as exc: _LOGGER.error( ( "Unable to publish: rendering topic template of %s " @@ -352,7 +352,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: payload = MqttCommandTemplate( template.Template(payload_template), hass=hass ).async_render() - except (jinja2.TemplateError, TemplateError) as exc: + except TEMPLATE_ERRORS as exc: _LOGGER.error( ( "Unable to publish to %s: rendering payload template of " diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index fba2f13937e..7f97910961d 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,6 +1,9 @@ """Constants used by multiple MQTT modules.""" +import jinja2 + from homeassistant.const import CONF_PAYLOAD, Platform +from homeassistant.exceptions import TemplateError ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_PAYLOAD = "discovery_payload" @@ -194,3 +197,5 @@ RELOADABLE_PLATFORMS = [ Platform.VALVE, Platform.WATER_HEATER, ] + +TEMPLATE_ERRORS = (jinja2.TemplateError, TemplateError, TypeError, ValueError) diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index 351eb422edc..c245b66fdb1 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -29,6 +29,7 @@ from .const import ( CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON, PAYLOAD_NONE, + TEMPLATE_ERRORS, ) from .debug_info import log_messages from .mixins import ( @@ -131,7 +132,10 @@ class MqttEvent(MqttEntity, EventEntity): return event_attributes: dict[str, Any] = {} event_type: str - payload = self._template(msg.payload, PayloadSentinel.DEFAULT) + try: + payload = self._template(msg.payload, PayloadSentinel.DEFAULT) + except TEMPLATE_ERRORS: + return if ( not payload or payload is PayloadSentinel.DEFAULT diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index 1f90f0fdb3d..e91a8c5c259 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -24,7 +24,7 @@ from homeassistant.util import dt as dt_util from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS +from .const import CONF_ENCODING, CONF_QOS, TEMPLATE_ERRORS from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -188,10 +188,11 @@ class MqttImage(MqttEntity, ImageEntity): @log_messages(self.hass, self.entity_id) def image_from_url_request_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - try: url = cv.url(self._url_template(msg.payload)) self._attr_image_url = url + except TEMPLATE_ERRORS: + return except vol.Invalid: _LOGGER.error( "Invalid image URL '%s' received at topic %s", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index aa42d257db4..c4529d18451 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -94,6 +94,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + TEMPLATE_ERRORS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -110,7 +111,6 @@ from .models import ( MqttValueTemplate, PublishPayloadType, ReceiveMessage, - ReceivePayloadType, ) from .subscription import ( EntitySubscription, @@ -480,7 +480,10 @@ def write_state_on_attr_change( attribute: getattr(entity, attribute, UNDEFINED) for attribute in attributes } - msg_callback(msg) + try: + msg_callback(msg) + except TEMPLATE_ERRORS: + return if not _attrs_have_changed(tracked_attrs): return @@ -527,8 +530,9 @@ class MqttAttributes(Entity): @log_messages(self.hass, self.entity_id) @write_state_on_attr_change(self, {"_attr_extra_state_attributes"}) def attributes_message_received(msg: ReceiveMessage) -> None: + """Update extra state attributes.""" + payload = attr_tpl(msg.payload) try: - payload = attr_tpl(msg.payload) json_dict = json_loads(payload) if isinstance(payload, str) else None if isinstance(json_dict, dict): filtered_dict = { @@ -636,7 +640,6 @@ class MqttAvailability(Entity): def availability_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT availability message.""" topic = msg.topic - payload: ReceivePayloadType payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload) if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: self._available[topic] = True diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 0d009cf356b..8b1f21c2775 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -29,6 +29,8 @@ if TYPE_CHECKING: from .discovery import MQTTDiscoveryPayload from .tag import MQTTTagScanner +from .const import TEMPLATE_ERRORS + class PayloadSentinel(StrEnum): """Sentinel for `async_render_with_possible_json_value`.""" @@ -247,7 +249,7 @@ class MqttValueTemplate: payload, variables=values ) ) - except Exception as exc: + except TEMPLATE_ERRORS as exc: _LOGGER.error( "%s: %s rendering template for entity '%s', template: '%s'", type(exc).__name__, @@ -255,7 +257,7 @@ class MqttValueTemplate: self._entity.entity_id if self._entity else "n/a", self._value_template.template, ) - raise exc + raise return rendered_payload _LOGGER.debug( @@ -274,18 +276,18 @@ class MqttValueTemplate: payload, default, variables=values ) ) - except Exception as ex: + except TEMPLATE_ERRORS as exc: _LOGGER.error( "%s: %s rendering template for entity '%s', template: " "'%s', default value: %s and payload: %s", - type(ex).__name__, - ex, + type(exc).__name__, + exc, self._entity.entity_id if self._entity else "n/a", self._value_template.template, default, payload, ) - raise ex + raise return rendered_payload diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 80a717b1f37..0eda584e95a 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -15,7 +15,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 +from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC, TEMPLATE_ERRORS from .discovery import MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -136,7 +136,10 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): """Subscribe to MQTT topics.""" async def tag_scanned(msg: ReceiveMessage) -> None: - tag_id = str(self._value_template(msg.payload, "")).strip() + try: + tag_id = str(self._value_template(msg.payload, "")).strip() + except TEMPLATE_ERRORS: + return if not tag_id: # No output from template, ignore return diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 40049431edb..89c5d2ffd91 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1354,3 +1354,32 @@ async def test_reload_after_invalid_config( # Make sure the config is loaded now assert hass.states.get("alarm_control_panel.test") is not None + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 3cc04b79e3a..033026226e2 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1294,3 +1294,31 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + binary_sensor.DOMAIN, + DEFAULT_CONFIG, + ( + { + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index b572753d6e6..0b44153ed93 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -29,6 +29,7 @@ from homeassistant.components.climate import ( from homeassistant.components.mqtt.climate import ( DEFAULT_INITIAL_TEMPERATURE, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, + VALUE_TEMPLATE_KEYS, ) from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant @@ -2514,3 +2515,36 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + climate.DOMAIN, + DEFAULT_CONFIG, + ( + { + value_template.replace("_value", "_state").replace( + "_template", "_topic" + ): "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template in VALUE_TEMPLATE_KEYS + ], + ids=VALUE_TEMPLATE_KEYS, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index df7b7a64b3d..3c3349d7f90 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -3550,3 +3550,43 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +VALUE_TEMPLATES = { + CONF_VALUE_TEMPLATE: CONF_STATE_TOPIC, + CONF_GET_POSITION_TEMPLATE: CONF_GET_POSITION_TOPIC, + CONF_TILT_STATUS_TEMPLATE: CONF_TILT_STATUS_TOPIC, +} + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + cover.DOMAIN, + DEFAULT_CONFIG, + ( + { + "position_topic": "position-topic", + "tilt_command_topic": "tilt-topic", + topic: "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template, topic in VALUE_TEMPLATES.items() + ], + ids=VALUE_TEMPLATES, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 204b149e479..bbed85c2e75 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -673,3 +673,31 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + device_tracker.DOMAIN, + DEFAULT_CONFIG, + ( + { + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 1a75d61c733..bd00120d098 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -802,3 +802,31 @@ async def test_skipped_async_ha_write_state2( async_fire_mqtt_message(hass, topic, payload2) await hass.async_block_till_done() assert len(mock_async_ha_write_state.mock_calls) == 2 + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + event.DOMAIN, + DEFAULT_CONFIG, + ( + { + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index e7c4eba54e2..8980d6951e5 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -2317,3 +2317,50 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +VALUE_TEMPLATES = { + "state_value_template": "state_topic", + "direction_value_template": "direction_state_topic", + "oscillation_value_template": "oscillation_state_topic", + "percentage_value_template": "percentage_state_topic", + "preset_mode_value_template": "preset_mode_state_topic", +} + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + fan.DOMAIN, + DEFAULT_CONFIG, + ( + { + "direction_command_topic": "direction-command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + ], + topic: "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template, topic in VALUE_TEMPLATES.items() + ], + ids=VALUE_TEMPLATES, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 69e85e51d73..d76fe84d0a9 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1613,3 +1613,47 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +VALUE_TEMPLATES = { + "state_value_template": "state_topic", + "action_template": "action_topic", + "mode_state_template": "mode_state_topic", + "current_humidity_template": "current_humidity_topic", + "target_humidity_state_template": "target_humidity_state_topic", +} + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + humidifier.DOMAIN, + DEFAULT_CONFIG, + ( + { + "mode_command_topic": "preset-mode-command-topic", + "modes": [ + "auto", + ], + topic: "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template, topic in VALUE_TEMPLATES.items() + ], + ids=VALUE_TEMPLATES, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 5ca9bbbc297..5a87f06653b 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -844,3 +844,31 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + image.DOMAIN: { + "name": "test", + "url_topic": "test-topic", + "url_template": "{{ value_json.some_var * 1 }}", + } + } + } + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1cfe2a2b29f..47567252f37 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -430,25 +430,27 @@ async def test_value_template_fails( entity.hass = hass tpl = template.Template("{{ value_json.some_var * 2 }}") val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) - with pytest.raises(TypeError): + with pytest.raises(TypeError) as exc: val_tpl.async_render_with_possible_json_value('{"some_var": null }') - await hass.async_block_till_done() + assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'" assert ( "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): + with pytest.raises(TypeError) 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 ( "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( diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index 85df2caef6c..d2680824fcd 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -924,3 +924,31 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "name": "test", + "activity_state_topic": "test-topic", + "activity_value_template": "{{ value_json.some_var * 1 }}", + } + } + } + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 7de6c08f269..aac79ae1893 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -186,6 +186,7 @@ from homeassistant.components.mqtt.light.schema_basic import ( CONF_RGBWW_COMMAND_TOPIC, CONF_XY_COMMAND_TOPIC, MQTT_LIGHT_ATTRIBUTES_BLOCKED, + VALUE_TEMPLATE_KEYS, ) from homeassistant.components.mqtt.models import PublishPayloadType from homeassistant.const import ( @@ -3691,3 +3692,36 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + value_template.replace( + "state_value_template", "state_topic" + ).replace("_value_template", "_state_topic"): "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template in VALUE_TEMPLATE_KEYS + ], + ids=VALUE_TEMPLATE_KEYS, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f9f355025e9..69e6b52de17 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1444,3 +1444,73 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +VALUE_TEMPLATES_NO_RGB = ( + "brightness_template", + "color_temp_template", + "effect_template", + "state_template", +) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template in VALUE_TEMPLATES_NO_RGB + ], + ids=VALUE_TEMPLATES_NO_RGB, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "red_template": "{{ value_json.r * 1 }}", + "green_template": "{{ value_json.g * 1 }}", + "blue_template": "{{ value_json.b * 1 }}", + }, + ), + ) + ], +) +async def test_rgb_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"r": 255, "g": 255, "b": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index e128590c907..082d328c17f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1079,3 +1079,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + lock.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 751521645a9..7b4b10ad776 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -396,3 +396,59 @@ async def test_name_attribute_is_set_or_not( assert state is not None assert state.attributes.get(ATTR_FRIENDLY_NAME) is None + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "availability_topic": "test-topic", + "availability_template": "{{ value_json.some_var * 1 }}", + } + } + }, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "availability": { + "topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + } + } + }, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "json_attributes_topic": "test-topic", + "json_attributes_template": "{{ value_json.some_var * 1 }}", + } + } + }, + ], + ids=[ + "availability_template1", + "availability_template2", + "json_attributes_template", + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index f69b6e0730a..bf4e51dc519 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -1175,3 +1175,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + number.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 030f5a2ac9a..5a17e7fc999 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -848,3 +848,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + select.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index e33d626c5d8..faa48012514 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1488,3 +1488,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + sensor.DOMAIN, + DEFAULT_CONFIG, + ( + { + "value_template": "{{ value_json.some_var * 1 }}", + "last_reset_value_template": "{{ value_json.some_var * 2 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 8a576068216..8d319d3a80b 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -1143,3 +1143,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + siren.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "state_value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 32195289aab..46a7ebb2060 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -800,3 +800,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + switch.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index cee7880cf1c..7f5a477e62a 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -951,3 +951,25 @@ async def test_unload_entry( async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) await hass.async_block_till_done() tag_mock.assert_not_called() + + +async def test_value_template_fails( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + tag_mock: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + config["value_template"] = "{{ value_json.some_var * 1 }}" + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "foobar/tag_scanned", '{"some_var": null }') + await hass.async_block_till_done() + + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index 3aa2f96f478..265f4afcb90 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -861,3 +861,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + text.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index c5fe5abd8c4..6a80e18ff23 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -782,3 +782,40 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +VALUE_TEMMPLATES = { + "value_template": "state_topic", + "latest_version_template": "latest_version_topic", +} + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + update.DOMAIN, + DEFAULT_CONFIG, + ( + { + state_topic: "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template, state_topic in VALUE_TEMMPLATES.items() + ], + ids=VALUE_TEMMPLATES, +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_valve.py b/tests/components/mqtt/test_valve.py index e37b52f56fb..6a10a038da7 100644 --- a/tests/components/mqtt/test_valve.py +++ b/tests/components/mqtt/test_valve.py @@ -1505,3 +1505,32 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + valve.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "value_template": "{{ value_json.some_var * 1 }}", + }, + ), + ) + ], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + ) diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 60c3af63bf4..61f430b34f8 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -1261,3 +1261,35 @@ async def test_skipped_async_ha_write_state( """Test a write state command is only called when there is change.""" await mqtt_mock_entry() await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + water_heater.DOMAIN, + DEFAULT_CONFIG, + ( + { + "modes": ["auto"], + "mode_state_topic": "test-topic", + value_template: "{{ value_json.some_var * 1 }}", + }, + ), + ) + for value_template in ["value_template", "mode_state_template"] + ], + ids=["value_template", "mode_state_template"], +) +async def test_value_template_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the rendering of MQTT value template fails.""" + await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", '{"some_var": null }') + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" + in caplog.text + )