From 3ca18922e6aa9a7bc6a24a9085b3932f3ac7bb51 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jan 2022 16:07:40 +0100 Subject: [PATCH] Mqtt rework on value templates (#62105) * add MqttValueTemplate class * support variables at initiation * pass MqttEntity instead of hass * Use MqttValueTemplace class for value templates * make hass en enitity parameters conditional * remove unused property and remove None assignment * rename self._attr_value_template --- homeassistant/components/mqtt/__init__.py | 58 ++++++++++ .../components/mqtt/alarm_control_panel.py | 16 +-- .../components/mqtt/binary_sensor.py | 36 +++--- homeassistant/components/mqtt/climate.py | 27 +++-- homeassistant/components/mqtt/cover.py | 109 +++++++----------- .../mqtt/device_tracker/schema_discovery.py | 13 +-- homeassistant/components/mqtt/fan.py | 11 +- homeassistant/components/mqtt/humidifier.py | 11 +- .../components/mqtt/light/schema_basic.py | 19 ++- .../components/mqtt/light/schema_template.py | 4 +- homeassistant/components/mqtt/lock.py | 14 +-- homeassistant/components/mqtt/mixins.py | 24 ++-- homeassistant/components/mqtt/number.py | 16 +-- homeassistant/components/mqtt/select.py | 16 +-- homeassistant/components/mqtt/sensor.py | 34 ++---- homeassistant/components/mqtt/switch.py | 13 +-- homeassistant/components/mqtt/tag.py | 14 +-- .../components/mqtt/vacuum/schema_legacy.py | 16 +-- tests/components/mqtt/test_init.py | 29 +++++ 19 files changed, 253 insertions(+), 227 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e4ae30c1e9a..1265cfd7b4b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -99,6 +99,8 @@ from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) +_SENTINEL = object() + DATA_MQTT = "mqtt" SERVICE_PUBLISH = "publish" @@ -317,6 +319,62 @@ class MqttCommandTemplate: ) +class MqttValueTemplate: + """Class for rendering MQTT value template with possible json values.""" + + def __init__( + self, + value_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + config_attributes: template.TemplateVarsType = None, + ) -> None: + """Instantiate a value template.""" + self._value_template = value_template + self._config_attributes = config_attributes + if value_template is None: + return + + value_template.hass = hass + self._entity = entity + + if entity: + value_template.hass = entity.hass + + @callback + def async_render_with_possible_json_value( + self, + payload: ReceivePayloadType, + default: ReceivePayloadType | object = _SENTINEL, + variables: template.TemplateVarsType = None, + ) -> ReceivePayloadType: + """Render with possible json value or pass-though a received MQTT value.""" + if self._value_template is None: + return payload + + values: dict[str, Any] = {} + + if variables is not None: + values.update(variables) + + if self._config_attributes is not None: + values.update(self._config_attributes) + + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + + if default == _SENTINEL: + return self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) + + return self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) + + @dataclass class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index dbb7b10536d..a2102b432f1 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -38,7 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -165,9 +165,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): return DISCOVERY_SCHEMA def _setup_from_config(self, config): - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value self._command_template = MqttCommandTemplate( self._config[CONF_COMMAND_TEMPLATE], entity=self ).async_render @@ -179,12 +180,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Run when new MQTT message has been received.""" - payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value( - msg.payload, self._state - ) + payload = self._value_template(msg.payload) if payload not in ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 2b87d6d0063..800db2cad79 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -30,7 +30,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttValueTemplate, subscription from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -119,9 +119,10 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): return DISCOVERY_SCHEMA def _setup_from_config(self, config): - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -137,7 +138,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle a new received MQTT state message.""" - payload = msg.payload # auto-expire enabled? expire_after = self._config.get(CONF_EXPIRE_AFTER) @@ -159,20 +159,16 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): self.hass, self._value_is_expired, expiration_at ) - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value( - payload, variables={"entity_id": self.entity_id} + payload = self._value_template(msg.payload) + if not payload.strip(): # No output from template, ignore + _LOGGER.debug( + "Empty template output for entity: %s with state topic: %s. Payload: '%s', with value template '%s'", + self._config[CONF_NAME], + self._config[CONF_STATE_TOPIC], + msg.payload, + self._config.get(CONF_VALUE_TEMPLATE), ) - if not payload.strip(): # No output from template, ignore - _LOGGER.debug( - "Empty template output for entity: %s with state topic: %s. Payload: '%s', with value template '%s'", - self._config[CONF_NAME], - self._config[CONF_STATE_TOPIC], - msg.payload, - value_template, - ) - return + return if payload == self._config[CONF_PAYLOAD_ON]: self._state = True @@ -180,8 +176,8 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): self._state = False else: # Payload is not for this entity template_info = "" - if value_template is not None: - template_info = f", template output: '{payload}', with value template '{str(value_template)}'" + if self._config.get(CONF_VALUE_TEMPLATE) is not None: + template_info = f", template output: '{payload}', with value template '{str(self._config.get(CONF_VALUE_TEMPLATE))}'" _LOGGER.info( "No matching payload found for entity: %s with state topic: %s. Payload: '%s'%s", self._config[CONF_NAME], diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 901aad8ec85..fdd0c44a2cb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -56,7 +56,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, MqttCommandTemplate, subscription +from . import ( + MQTT_BASE_PLATFORM_SCHEMA, + PLATFORMS, + MqttCommandTemplate, + MqttValueTemplate, + subscription, +) from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN from .debug_info import log_messages @@ -372,19 +378,20 @@ class MqttClimate(MqttEntity, ClimateEntity): value_templates = {} for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = lambda value: value + value_templates[key] = None if CONF_VALUE_TEMPLATE in config: - value_template = config.get(CONF_VALUE_TEMPLATE) - value_template.hass = self.hass value_templates = { - key: value_template.async_render_with_possible_json_value - for key in VALUE_TEMPLATE_KEYS + key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS } for key in VALUE_TEMPLATE_KEYS & config.keys(): - tpl = config[key] - value_templates[key] = tpl.async_render_with_possible_json_value - tpl.hass = self.hass - self._value_templates = value_templates + value_templates[key] = config[key] + self._value_templates = { + key: MqttValueTemplate( + template, + entity=self, + ).async_render_with_possible_json_value + for key, template in value_templates.items() + } command_templates = {} for key in COMMAND_TEMPLATE_KEYS: diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 014c21d384e..5bdf996aa34 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -40,7 +40,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -303,27 +303,39 @@ class MqttCover(MqttEntity, CoverEntity): # Force into optimistic tilt mode. self._tilt_optimistic = True - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + template_config_attributes = { + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value self._set_position_template = MqttCommandTemplate( self._config.get(CONF_SET_POSITION_TEMPLATE), entity=self ).async_render - get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE) - if get_position_template is not None: - get_position_template.hass = self.hass + self._get_position_template = MqttValueTemplate( + self._config.get(CONF_GET_POSITION_TEMPLATE), + entity=self, + config_attributes=template_config_attributes, + ).async_render_with_possible_json_value self._set_tilt_template = MqttCommandTemplate( self._config.get(CONF_TILT_COMMAND_TEMPLATE), entity=self ).async_render - tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) - if tilt_status_template is not None: - tilt_status_template.hass = self.hass + self._tilt_status_template = MqttValueTemplate( + self._config.get(CONF_TILT_STATUS_TEMPLATE), + entity=self, + config_attributes=template_config_attributes, + ).async_render_with_possible_json_value - async def _subscribe_topics(self): # noqa: C901 + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -331,19 +343,7 @@ class MqttCover(MqttEntity, CoverEntity): @log_messages(self.hass, self.entity_id) def tilt_message_received(msg): """Handle tilt updates.""" - payload = msg.payload - template = self._config.get(CONF_TILT_STATUS_TEMPLATE) - if template is not None: - variables = { - "entity_id": self.entity_id, - "position_open": self._config[CONF_POSITION_OPEN], - "position_closed": self._config[CONF_POSITION_CLOSED], - "tilt_min": self._config[CONF_TILT_MIN], - "tilt_max": self._config[CONF_TILT_MAX], - } - payload = template.async_render_with_possible_json_value( - payload, variables=variables - ) + payload = self._tilt_status_template(msg.payload) if not payload: _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) @@ -355,13 +355,7 @@ class MqttCover(MqttEntity, CoverEntity): @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" - payload = msg.payload - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - variables = {"entity_id": self.entity_id} - payload = template.async_render_with_possible_json_value( - payload, variables=variables - ) + payload = self._value_template(msg.payload) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) @@ -399,44 +393,29 @@ class MqttCover(MqttEntity, CoverEntity): @log_messages(self.hass, self.entity_id) def position_message_received(msg): """Handle new MQTT position messages.""" - payload = msg.payload + payload = self._get_position_template(msg.payload) - template = self._config.get(CONF_GET_POSITION_TEMPLATE) - if template is not None: - variables = { - "entity_id": self.entity_id, - "position_open": self._config[CONF_POSITION_OPEN], - "position_closed": self._config[CONF_POSITION_CLOSED], - "tilt_min": self._config[CONF_TILT_MIN], - "tilt_max": self._config[CONF_TILT_MAX], - } - payload = template.async_render_with_possible_json_value( - payload, variables=variables - ) + if not payload: + _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) + return - if not payload: - _LOGGER.debug( - "Ignoring empty position message from '%s'", msg.topic + try: + payload = json_loads(payload) + except JSONDecodeError: + pass + + if isinstance(payload, dict): + if "position" not in payload: + _LOGGER.warning( + "Template (position_template) returned JSON without position attribute" ) return - - try: - payload = json_loads(payload) - except JSONDecodeError: - pass - - if isinstance(payload, dict): - if "position" not in payload: - _LOGGER.warning( - "Template (position_template) returned JSON without position attribute" - ) - return - if "tilt_position" in payload: - if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): - # reset forced set tilt optimistic - self._tilt_optimistic = False - self.tilt_payload_received(payload["tilt_position"]) - payload = payload["position"] + if "tilt_position" in payload: + if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): + # reset forced set tilt optimistic + self._tilt_optimistic = False + self.tilt_payload_received(payload["tilt_position"]) + payload = payload["position"] try: percentage_payload = self.find_percentage_in_range( diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1ccf03423de..3ee5f22be90 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import subscription +from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages @@ -73,9 +73,9 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -84,10 +84,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + payload = self._value_template(msg.payload) if payload == self._config[CONF_PAYLOAD_HOME]: self._location_name = STATE_HOME elif payload == self._config[CONF_PAYLOAD_NOT_HOME]: diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b98a63b548f..5818c38a270 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -40,7 +40,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -356,11 +356,10 @@ class MqttFan(MqttEntity, FanEntity): ).async_render for key, tpl in self._value_templates.items(): - if tpl is None: - self._value_templates[key] = lambda value: value - else: - tpl.hass = self.hass - self._value_templates[key] = tpl.async_render_with_possible_json_value + self._value_templates[key] = MqttValueTemplate( + tpl, + entity=self, + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 4fcdb177253..08fc3a41108 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -262,11 +262,10 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ).async_render for key, tpl in self._value_templates.items(): - if tpl is None: - self._value_templates[key] = lambda value: value - else: - tpl.hass = self.hass - self._value_templates[key] = tpl.async_render_with_possible_json_value + self._value_templates[key] = MqttValueTemplate( + tpl, + entity=self, + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 90f60769f67..ce17495d4f7 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttCommandTemplate, subscription +from .. import MqttCommandTemplate, MqttValueTemplate, subscription from ... import mqtt from ..const import ( CONF_COMMAND_TOPIC, @@ -325,12 +325,19 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): value_templates = {} for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = lambda value, _: value + value_templates[key] = None + if CONF_VALUE_TEMPLATE in config: + value_templates = { + key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS + } for key in VALUE_TEMPLATE_KEYS & config.keys(): - tpl = config[key] - value_templates[key] = tpl.async_render_with_possible_json_value - tpl.hass = self.hass - self._value_templates = value_templates + value_templates[key] = config[key] + self._value_templates = { + key: MqttValueTemplate( + template, entity=self + ).async_render_with_possible_json_value + for key, template in value_templates.items() + } command_templates = {} for key in COMMAND_TEMPLATE_KEYS: diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index ff9058384e0..ee0ac97d3ad 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -33,7 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import subscription +from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import ( CONF_COMMAND_TOPIC, @@ -160,7 +160,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: - tpl.hass = self.hass + tpl = MqttValueTemplate(tpl, entity=self) last_state = await self.async_get_last_state() diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index baed3a28e1e..7e701e05e7d 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -118,9 +118,10 @@ class MqttLock(MqttEntity, LockEntity): """(Re)Setup the entity.""" self._optimistic = config[CONF_OPTIMISTIC] - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = self.hass + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -129,10 +130,7 @@ class MqttLock(MqttEntity, LockEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + payload = self._value_template(msg.payload) if payload == self._config[CONF_STATE_LOCKED]: self._state = True elif payload == self._config[CONF_STATE_UNLOCKED]: diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9b6d4323433..ff885182514 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -38,7 +38,7 @@ from homeassistant.helpers.entity import ( ) from homeassistant.helpers.typing import ConfigType -from . import DATA_MQTT, debug_info, publish, subscription +from . import DATA_MQTT, MqttValueTemplate, debug_info, publish, subscription from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -254,17 +254,15 @@ class MqttAttributes(Entity): async def _attributes_subscribe_topics(self): """(Re)Subscribe to topics.""" - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) - if attr_tpl is not None: - attr_tpl.hass = self.hass + attr_tpl = MqttValueTemplate( + self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE), entity=self + ).async_render_with_possible_json_value @callback @log_messages(self.hass, self.entity_id) def attributes_message_received(msg: ReceiveMessage) -> None: try: - payload = msg.payload - if attr_tpl is not None: - payload = attr_tpl.async_render_with_possible_json_value(payload) + payload = attr_tpl(msg.payload) json_dict = json.loads(payload) if isinstance(payload, str) else None if isinstance(json_dict, dict): filtered_dict = { @@ -356,14 +354,10 @@ class MqttAvailability(Entity): topic, # pylint: disable=unused-variable avail_topic_conf, ) in self._avail_topics.items(): - tpl = avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] - if tpl is None: - avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = lambda value: value - else: - tpl.hass = self.hass - avail_topic_conf[ - CONF_AVAILABILITY_TEMPLATE - ] = tpl.async_render_with_possible_json_value + avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = MqttValueTemplate( + avail_topic_conf[CONF_AVAILABILITY_TEMPLATE], + entity=self, + ).async_render_with_possible_json_value self._avail_config = config diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e3cc15a4944..7a36da8baa1 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -27,7 +27,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -157,18 +157,12 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): CONF_COMMAND_TEMPLATE: MqttCommandTemplate( config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render, - CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), + CONF_VALUE_TEMPLATE: MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value, } - value_template = self._templates[CONF_VALUE_TEMPLATE] - if value_template is None: - self._templates[CONF_VALUE_TEMPLATE] = lambda value: value - else: - value_template.hass = self.hass - self._templates[ - CONF_VALUE_TEMPLATE - ] = value_template.async_render_with_possible_json_value - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 4569b5c6610..b315d5c8ec7 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -17,7 +17,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, subscription +from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -123,18 +123,12 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): CONF_COMMAND_TEMPLATE: MqttCommandTemplate( config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render, - CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), + CONF_VALUE_TEMPLATE: MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value, } - value_template = self._templates[CONF_VALUE_TEMPLATE] - if value_template is None: - self._templates[CONF_VALUE_TEMPLATE] = lambda value: value - else: - value_template.hass = self.hass - self._templates[ - CONF_VALUE_TEMPLATE - ] = value_template.async_render_with_possible_json_value - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index ebfaf7c47c1..47fa9793166 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttValueTemplate, subscription from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -167,19 +167,18 @@ class MqttSensor(MqttEntity, SensorEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - template.hass = self.hass - last_reset_template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE) - if last_reset_template is not None: - last_reset_template.hass = self.hass + self._template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value + self._last_reset_template = MqttValueTemplate( + self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} def _update_state(msg): - payload = msg.payload # auto-expire enabled? expire_after = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: @@ -198,14 +197,7 @@ class MqttSensor(MqttEntity, SensorEntity): self.hass, self._value_is_expired, expiration_at ) - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - variables = {"entity_id": self.entity_id} - payload = template.async_render_with_possible_json_value( - payload, - self._state, - variables=variables, - ) + payload = self._template(msg.payload) if payload is not None and self.device_class in ( SensorDeviceClass.DATE, @@ -221,16 +213,8 @@ class MqttSensor(MqttEntity, SensorEntity): self._state = payload def _update_last_reset(msg): - payload = msg.payload + payload = self._last_reset_template(msg.payload) - template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE) - if template is not None: - variables = {"entity_id": self.entity_id} - payload = template.async_render_with_possible_json_value( - payload, - self._state, - variables=variables, - ) if not payload: _LOGGER.debug("Ignoring empty last_reset message from '%s'", msg.topic) return diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 0fdcebe6ff2..b6ad3307ee7 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -24,7 +24,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -128,9 +128,9 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._optimistic = config[CONF_OPTIMISTIC] - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - template.hass = self.hass + self._value_template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -139,10 +139,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" - payload = msg.payload - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - payload = template.async_render_with_possible_json_value(payload) + payload = self._value_template(msg.payload) if payload == self._state_on: self._state = True elif payload == self._state_off: diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index a228d56d5c7..b2638f8ac4b 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import subscription +from . import MqttValueTemplate, subscription from .. import mqtt from .const import ( ATTR_DISCOVERY_HASH, @@ -143,12 +143,10 @@ class MQTTTagScanner: ) def _setup_from_config(self, config): - self._value_template = lambda value, error_value: value - if CONF_VALUE_TEMPLATE in config: - value_template = config.get(CONF_VALUE_TEMPLATE) - value_template.hass = self.hass - - self._value_template = value_template.async_render_with_possible_json_value + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + hass=self.hass, + ).async_render_with_possible_json_value async def setup(self): """Set up the MQTT tag scanner.""" @@ -171,7 +169,7 @@ class MQTTTagScanner: """Subscribe to MQTT topics.""" async def tag_scanned(msg): - tag_id = self._value_template(msg.payload, error_value="").strip() + tag_id = self._value_template(msg.payload, "").strip() if not tag_id: # No output from template, ignore return diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index ca538fe3cb6..08a0cad76cc 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -24,7 +24,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import subscription +from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages @@ -244,7 +244,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: - tpl.hass = self.hass + tpl = MqttValueTemplate(tpl, entity=self) @callback @log_messages(self.hass, self.entity_id) @@ -256,7 +256,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): battery_level = self._templates[ CONF_BATTERY_LEVEL_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if battery_level: self._battery_level = int(battery_level) @@ -266,7 +266,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): charging = self._templates[ CONF_CHARGING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if charging: self._charging = cv.boolean(charging) @@ -276,7 +276,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): cleaning = self._templates[ CONF_CLEANING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if cleaning: self._cleaning = cv.boolean(cleaning) @@ -286,7 +286,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): docked = self._templates[ CONF_DOCKED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if docked: self._docked = cv.boolean(docked) @@ -296,7 +296,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): error = self._templates[ CONF_ERROR_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if error is not None: self._error = cv.string(error) @@ -318,7 +318,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): ): fan_speed = self._templates[ CONF_FAN_SPEED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, error_value=None) + ].async_render_with_possible_json_value(msg.payload, None) if fan_speed: self._fan_speed = fan_speed diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b2d468a66b8..f55a36ed189 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -238,6 +238,35 @@ async def test_command_template_variables(hass, mqtt_mock): assert state.state == "beer" +async def test_value_template_value(hass): + """Test the rendering of MQTT value template.""" + + variables = {"id": 1234, "some_var": "beer"} + + # test rendering value + tpl = template.Template("{{ value_json.id }}", hass) + val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass) + assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" + + # test variables at rendering + tpl = template.Template("{{ value_json.id }} {{ some_var }}", hass) + val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass) + assert ( + val_tpl.async_render_with_possible_json_value( + '{"id": 4321}', variables=variables + ) + == "4321 beer" + ) + + # test with default value if an error occurs due to an invalid template + tpl = template.Template("{{ value_json.id | as_datetime }}") + val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass) + assert ( + val_tpl.async_render_with_possible_json_value('{"otherid": 4321}', "my default") + == "my default" + ) + + async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): """Test the service call if topic is missing.""" with pytest.raises(vol.Invalid):