Handle template errors on MQTT payload handling (#110180)

* Handle template errors on MQTT payload handling (alt)

* Handle mqtt event en image template errors correctly
This commit is contained in:
Jan Bouwhuis 2024-02-13 10:59:55 +01:00 committed by GitHub
parent ee25f6b960
commit 09f1ec78a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 859 additions and 22 deletions

View file

@ -7,7 +7,6 @@ from datetime import datetime
import logging import logging
from typing import TYPE_CHECKING, Any, TypeVar, cast from typing import TYPE_CHECKING, Any, TypeVar, cast
import jinja2
import voluptuous as vol import voluptuous as vol
from homeassistant import config as conf_util from homeassistant import config as conf_util
@ -27,7 +26,6 @@ from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ( from homeassistant.exceptions import (
ConfigValidationError, ConfigValidationError,
ServiceValidationError, ServiceValidationError,
TemplateError,
Unauthorized, Unauthorized,
) )
from homeassistant.helpers import config_validation as cv, event as ev, template from homeassistant.helpers import config_validation as cv, event as ev, template
@ -87,11 +85,13 @@ from .const import ( # noqa: F401
MQTT_DISCONNECTED, MQTT_DISCONNECTED,
PLATFORMS, PLATFORMS,
RELOADABLE_PLATFORMS, RELOADABLE_PLATFORMS,
TEMPLATE_ERRORS,
) )
from .models import ( # noqa: F401 from .models import ( # noqa: F401
MqttCommandTemplate, MqttCommandTemplate,
MqttData, MqttData,
MqttValueTemplate, MqttValueTemplate,
PayloadSentinel,
PublishPayloadType, PublishPayloadType,
ReceiveMessage, ReceiveMessage,
ReceivePayloadType, ReceivePayloadType,
@ -325,7 +325,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
msg_topic_template, hass msg_topic_template, hass
).async_render(parse_result=False) ).async_render(parse_result=False)
msg_topic = valid_publish_topic(rendered_topic) msg_topic = valid_publish_topic(rendered_topic)
except (jinja2.TemplateError, TemplateError) as exc: except TEMPLATE_ERRORS as exc:
_LOGGER.error( _LOGGER.error(
( (
"Unable to publish: rendering topic template of %s " "Unable to publish: rendering topic template of %s "
@ -352,7 +352,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
payload = MqttCommandTemplate( payload = MqttCommandTemplate(
template.Template(payload_template), hass=hass template.Template(payload_template), hass=hass
).async_render() ).async_render()
except (jinja2.TemplateError, TemplateError) as exc: except TEMPLATE_ERRORS as exc:
_LOGGER.error( _LOGGER.error(
( (
"Unable to publish to %s: rendering payload template of " "Unable to publish to %s: rendering payload template of "

View file

@ -1,6 +1,9 @@
"""Constants used by multiple MQTT modules.""" """Constants used by multiple MQTT modules."""
import jinja2
from homeassistant.const import CONF_PAYLOAD, Platform from homeassistant.const import CONF_PAYLOAD, Platform
from homeassistant.exceptions import TemplateError
ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_HASH = "discovery_hash"
ATTR_DISCOVERY_PAYLOAD = "discovery_payload" ATTR_DISCOVERY_PAYLOAD = "discovery_payload"
@ -194,3 +197,5 @@ RELOADABLE_PLATFORMS = [
Platform.VALVE, Platform.VALVE,
Platform.WATER_HEATER, Platform.WATER_HEATER,
] ]
TEMPLATE_ERRORS = (jinja2.TemplateError, TemplateError, TypeError, ValueError)

View file

@ -29,6 +29,7 @@ from .const import (
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
PAYLOAD_EMPTY_JSON, PAYLOAD_EMPTY_JSON,
PAYLOAD_NONE, PAYLOAD_NONE,
TEMPLATE_ERRORS,
) )
from .debug_info import log_messages from .debug_info import log_messages
from .mixins import ( from .mixins import (
@ -131,7 +132,10 @@ class MqttEvent(MqttEntity, EventEntity):
return return
event_attributes: dict[str, Any] = {} event_attributes: dict[str, Any] = {}
event_type: str event_type: str
payload = self._template(msg.payload, PayloadSentinel.DEFAULT) try:
payload = self._template(msg.payload, PayloadSentinel.DEFAULT)
except TEMPLATE_ERRORS:
return
if ( if (
not payload not payload
or payload is PayloadSentinel.DEFAULT or payload is PayloadSentinel.DEFAULT

View file

@ -24,7 +24,7 @@ from homeassistant.util import dt as dt_util
from . import subscription from . import subscription
from .config import MQTT_BASE_SCHEMA 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 .debug_info import log_messages
from .mixins import ( from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA, MQTT_ENTITY_COMMON_SCHEMA,
@ -188,10 +188,11 @@ class MqttImage(MqttEntity, ImageEntity):
@log_messages(self.hass, self.entity_id) @log_messages(self.hass, self.entity_id)
def image_from_url_request_received(msg: ReceiveMessage) -> None: def image_from_url_request_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
try: try:
url = cv.url(self._url_template(msg.payload)) url = cv.url(self._url_template(msg.payload))
self._attr_image_url = url self._attr_image_url = url
except TEMPLATE_ERRORS:
return
except vol.Invalid: except vol.Invalid:
_LOGGER.error( _LOGGER.error(
"Invalid image URL '%s' received at topic %s", "Invalid image URL '%s' received at topic %s",

View file

@ -94,6 +94,7 @@ from .const import (
DOMAIN, DOMAIN,
MQTT_CONNECTED, MQTT_CONNECTED,
MQTT_DISCONNECTED, MQTT_DISCONNECTED,
TEMPLATE_ERRORS,
) )
from .debug_info import log_message, log_messages from .debug_info import log_message, log_messages
from .discovery import ( from .discovery import (
@ -110,7 +111,6 @@ from .models import (
MqttValueTemplate, MqttValueTemplate,
PublishPayloadType, PublishPayloadType,
ReceiveMessage, ReceiveMessage,
ReceivePayloadType,
) )
from .subscription import ( from .subscription import (
EntitySubscription, EntitySubscription,
@ -480,7 +480,10 @@ def write_state_on_attr_change(
attribute: getattr(entity, attribute, UNDEFINED) attribute: getattr(entity, attribute, UNDEFINED)
for attribute in attributes for attribute in attributes
} }
msg_callback(msg) try:
msg_callback(msg)
except TEMPLATE_ERRORS:
return
if not _attrs_have_changed(tracked_attrs): if not _attrs_have_changed(tracked_attrs):
return return
@ -527,8 +530,9 @@ class MqttAttributes(Entity):
@log_messages(self.hass, self.entity_id) @log_messages(self.hass, self.entity_id)
@write_state_on_attr_change(self, {"_attr_extra_state_attributes"}) @write_state_on_attr_change(self, {"_attr_extra_state_attributes"})
def attributes_message_received(msg: ReceiveMessage) -> None: def attributes_message_received(msg: ReceiveMessage) -> None:
"""Update extra state attributes."""
payload = attr_tpl(msg.payload)
try: try:
payload = attr_tpl(msg.payload)
json_dict = json_loads(payload) if isinstance(payload, str) else None json_dict = json_loads(payload) if isinstance(payload, str) else None
if isinstance(json_dict, dict): if isinstance(json_dict, dict):
filtered_dict = { filtered_dict = {
@ -636,7 +640,6 @@ class MqttAvailability(Entity):
def availability_message_received(msg: ReceiveMessage) -> None: def availability_message_received(msg: ReceiveMessage) -> None:
"""Handle a new received MQTT availability message.""" """Handle a new received MQTT availability message."""
topic = msg.topic topic = msg.topic
payload: ReceivePayloadType
payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload) payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload)
if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]:
self._available[topic] = True self._available[topic] = True

View file

@ -29,6 +29,8 @@ if TYPE_CHECKING:
from .discovery import MQTTDiscoveryPayload from .discovery import MQTTDiscoveryPayload
from .tag import MQTTTagScanner from .tag import MQTTTagScanner
from .const import TEMPLATE_ERRORS
class PayloadSentinel(StrEnum): class PayloadSentinel(StrEnum):
"""Sentinel for `async_render_with_possible_json_value`.""" """Sentinel for `async_render_with_possible_json_value`."""
@ -247,7 +249,7 @@ class MqttValueTemplate:
payload, variables=values payload, variables=values
) )
) )
except Exception as exc: except TEMPLATE_ERRORS as exc:
_LOGGER.error( _LOGGER.error(
"%s: %s rendering template for entity '%s', template: '%s'", "%s: %s rendering template for entity '%s', template: '%s'",
type(exc).__name__, type(exc).__name__,
@ -255,7 +257,7 @@ class MqttValueTemplate:
self._entity.entity_id if self._entity else "n/a", self._entity.entity_id if self._entity else "n/a",
self._value_template.template, self._value_template.template,
) )
raise exc raise
return rendered_payload return rendered_payload
_LOGGER.debug( _LOGGER.debug(
@ -274,18 +276,18 @@ class MqttValueTemplate:
payload, default, variables=values payload, default, variables=values
) )
) )
except Exception as ex: except TEMPLATE_ERRORS as exc:
_LOGGER.error( _LOGGER.error(
"%s: %s rendering template for entity '%s', template: " "%s: %s rendering template for entity '%s', template: "
"'%s', default value: %s and payload: %s", "'%s', default value: %s and payload: %s",
type(ex).__name__, type(exc).__name__,
ex, exc,
self._entity.entity_id if self._entity else "n/a", self._entity.entity_id if self._entity else "n/a",
self._value_template.template, self._value_template.template,
default, default,
payload, payload,
) )
raise ex raise
return rendered_payload return rendered_payload

View file

@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import subscription from . import subscription
from .config import MQTT_BASE_SCHEMA 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 .discovery import MQTTDiscoveryPayload
from .mixins import ( from .mixins import (
MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA,
@ -136,7 +136,10 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate):
"""Subscribe to MQTT topics.""" """Subscribe to MQTT topics."""
async def tag_scanned(msg: ReceiveMessage) -> None: 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 if not tag_id: # No output from template, ignore
return return

View file

@ -1354,3 +1354,32 @@ async def test_reload_after_invalid_config(
# Make sure the config is loaded now # Make sure the config is loaded now
assert hass.states.get("alarm_control_panel.test") is not None 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
)

View file

@ -1294,3 +1294,31 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -29,6 +29,7 @@ from homeassistant.components.climate import (
from homeassistant.components.mqtt.climate import ( from homeassistant.components.mqtt.climate import (
DEFAULT_INITIAL_TEMPERATURE, DEFAULT_INITIAL_TEMPERATURE,
MQTT_CLIMATE_ATTRIBUTES_BLOCKED, MQTT_CLIMATE_ATTRIBUTES_BLOCKED,
VALUE_TEMPLATE_KEYS,
) )
from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant 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.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -3550,3 +3550,43 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -673,3 +673,31 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -802,3 +802,31 @@ async def test_skipped_async_ha_write_state2(
async_fire_mqtt_message(hass, topic, payload2) async_fire_mqtt_message(hass, topic, payload2)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_async_ha_write_state.mock_calls) == 2 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
)

View file

@ -2317,3 +2317,50 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1613,3 +1613,47 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -844,3 +844,31 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -430,25 +430,27 @@ async def test_value_template_fails(
entity.hass = hass entity.hass = hass
tpl = template.Template("{{ value_json.some_var * 2 }}") tpl = template.Template("{{ value_json.some_var * 2 }}")
val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) 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 }') 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 ( assert (
"TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' "
"rendering template for entity 'sensor.test', " "rendering template for entity 'sensor.test', "
"template: '{{ value_json.some_var * 2 }}'" "template: '{{ value_json.some_var * 2 }}'"
) in caplog.text ) in caplog.text
caplog.clear() caplog.clear()
with pytest.raises(TypeError): with pytest.raises(TypeError) as exc:
val_tpl.async_render_with_possible_json_value( val_tpl.async_render_with_possible_json_value(
'{"some_var": null }', default=100 '{"some_var": null }', default=100
) )
assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'"
assert ( assert (
"TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' "
"rendering template for entity 'sensor.test', " "rendering template for entity 'sensor.test', "
"template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: " "template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: "
'{"some_var": null }' '{"some_var": null }'
) in caplog.text ) in caplog.text
await hass.async_block_till_done()
async def test_service_call_without_topic_does_not_publish( async def test_service_call_without_topic_does_not_publish(

View file

@ -924,3 +924,31 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -186,6 +186,7 @@ from homeassistant.components.mqtt.light.schema_basic import (
CONF_RGBWW_COMMAND_TOPIC, CONF_RGBWW_COMMAND_TOPIC,
CONF_XY_COMMAND_TOPIC, CONF_XY_COMMAND_TOPIC,
MQTT_LIGHT_ATTRIBUTES_BLOCKED, MQTT_LIGHT_ATTRIBUTES_BLOCKED,
VALUE_TEMPLATE_KEYS,
) )
from homeassistant.components.mqtt.models import PublishPayloadType from homeassistant.components.mqtt.models import PublishPayloadType
from homeassistant.const import ( 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.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1444,3 +1444,73 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1079,3 +1079,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -396,3 +396,59 @@ async def test_name_attribute_is_set_or_not(
assert state is not None assert state is not None
assert state.attributes.get(ATTR_FRIENDLY_NAME) is 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
)

View file

@ -1175,3 +1175,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -848,3 +848,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1488,3 +1488,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1143,3 +1143,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -800,3 +800,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -951,3 +951,25 @@ async def test_unload_entry(
async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN)
await hass.async_block_till_done() await hass.async_block_till_done()
tag_mock.assert_not_called() 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
)

View file

@ -861,3 +861,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -782,3 +782,40 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1505,3 +1505,32 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)

View file

@ -1261,3 +1261,35 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) 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
)