diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a083e12dd31..6202cb92a72 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -23,6 +23,8 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_NAME, CONF_CLIENT_ID, CONF_DISCOVERY, CONF_PASSWORD, @@ -47,6 +49,7 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.loader import bind_hass @@ -259,15 +262,22 @@ class MqttCommandTemplate: def __init__( self, command_template: template.Template | None, - hass: HomeAssistant, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, ) -> None: """Instantiate a command template.""" self._attr_command_template = command_template if command_template is None: return + self._entity = entity + command_template.hass = hass + if entity: + command_template.hass = entity.hass + @callback def async_render( self, @@ -295,6 +305,9 @@ class MqttCommandTemplate: return value values = {"value": value} + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name if variables is not None: values.update(variables) return _convert_outgoing_payload( @@ -613,7 +626,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if payload_template is not None: try: payload = MqttCommandTemplate( - template.Template(payload_template), hass + template.Template(payload_template), hass=hass ).async_render() except (template.jinja2.TemplateError, TemplateError) as exc: _LOGGER.error( diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c90875010b2..dbb7b10536d 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -169,7 +169,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): if value_template is not None: value_template.hass = self.hass self._command_template = MqttCommandTemplate( - self._config[CONF_COMMAND_TEMPLATE], self.hass + self._config[CONF_COMMAND_TEMPLATE], entity=self ).async_render async def _subscribe_topics(self): diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 1c0a2ac2a53..901aad8ec85 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -389,7 +389,7 @@ class MqttClimate(MqttEntity, ClimateEntity): command_templates = {} for key in COMMAND_TEMPLATE_KEYS: command_templates[key] = MqttCommandTemplate( - config.get(key), self.hass + config.get(key), entity=self ).async_render self._command_templates = command_templates diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 6d3701f8ebc..2ba06567e85 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -308,7 +308,7 @@ class MqttCover(MqttEntity, CoverEntity): value_template.hass = self.hass self._set_position_template = MqttCommandTemplate( - self._config.get(CONF_SET_POSITION_TEMPLATE), self.hass + self._config.get(CONF_SET_POSITION_TEMPLATE), entity=self ).async_render get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE) @@ -316,7 +316,7 @@ class MqttCover(MqttEntity, CoverEntity): get_position_template.hass = self.hass self._set_tilt_template = MqttCommandTemplate( - self._config.get(CONF_TILT_COMMAND_TEMPLATE), self.hass + self._config.get(CONF_TILT_COMMAND_TEMPLATE), entity=self ).async_render tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 0d1a623eaea..b98a63b548f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -352,7 +352,7 @@ class MqttFan(MqttEntity, FanEntity): for key, tpl in self._command_templates.items(): self._command_templates[key] = MqttCommandTemplate( - tpl, self.hass + tpl, entity=self ).async_render for key, tpl in self._value_templates.items(): diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 0d4dde8bff2..4fcdb177253 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -258,7 +258,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): for key, tpl in self._command_templates.items(): self._command_templates[key] = MqttCommandTemplate( - tpl, self.hass + tpl, entity=self ).async_render for key, tpl in self._value_templates.items(): diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index ee83560c8b3..90f60769f67 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 subscription +from .. import MqttCommandTemplate, subscription from ... import mqtt from ..const import ( CONF_COMMAND_TOPIC, @@ -336,9 +336,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): for key in COMMAND_TEMPLATE_KEYS: command_templates[key] = None for key in COMMAND_TEMPLATE_KEYS & config.keys(): - tpl = config[key] - command_templates[key] = tpl.async_render - tpl.hass = self.hass + command_templates[key] = MqttCommandTemplate( + config[key], entity=self + ).async_render self._command_templates = command_templates optimistic = config[CONF_OPTIMISTIC] @@ -851,7 +851,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): keys.append("white") elif color_mode == COLOR_MODE_RGBWW: keys.extend(["cold_white", "warm_white"]) - rgb_color_str = tpl(zip(keys, color)) + rgb_color_str = tpl(variables=zip(keys, color)) else: rgb_color_str = ",".join(str(channel) for channel in color) return rgb_color_str @@ -1017,9 +1017,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None ): color_temp = int(kwargs[ATTR_COLOR_TEMP]) - tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] - if tpl: - color_temp = tpl({"value": color_temp}) + if tpl := self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE]: + color_temp = tpl(variables={"value": color_temp}) await publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) should_update |= set_optimistic( diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index fd849e3731b..e3cc15a4944 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -155,7 +155,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._templates = { CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), self.hass + config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render, CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), } diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 195f1753d8f..4569b5c6610 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -121,7 +121,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): self._templates = { CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), self.hass + config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render, CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), } diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 32528881d64..b2d468a66b8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,10 +12,12 @@ from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.const import ( + ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, ) +import homeassistant.core as ha from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, template @@ -158,7 +160,7 @@ async def test_publish(hass, mqtt_mock): async def test_convert_outgoing_payload(hass): """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass) + command_template = mqtt.MqttCommandTemplate(None, hass=hass) assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" assert ( @@ -179,16 +181,63 @@ async def test_command_template_value(hass): variables = {"id": 1234, "some_var": "beer"} # test rendering value - tpl = template.Template("{{ value + 1 }}", hass) - cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) + tpl = template.Template("{{ value + 1 }}", hass=hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass) assert cmd_tpl.async_render(4321) == "4322" # test variables at rendering - tpl = template.Template("{{ some_var }}", hass) - cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) + tpl = template.Template("{{ some_var }}", hass=hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass) assert cmd_tpl.async_render(None, variables=variables) == "beer" +async def test_command_template_variables(hass, mqtt_mock): + """Test the rendering of enitity_variables.""" + topic = "test/select" + + fake_state = ha.State("select.test", "milk") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}"}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "milk" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.test_select", "option": "beer"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + topic, + '{"option": "beer", "entity_id": "select.test_select", "name": "Test Select"}', + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("select.test_select") + assert state.state == "beer" + + 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):