Add mqtt entity attributes command templates (#61937)

* Add entity variables to MqttCommandTemplate

* missing command template update

* make hass and entity conditional parameters

* Add encoding support for publishing

* Revert "Add encoding support for publishing"

This reverts commit b69b9c60ec.
This commit is contained in:
Jan Bouwhuis 2022-01-03 15:10:15 +01:00 committed by GitHub
parent 7f9b7c7b0e
commit 457ce195dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 23 deletions

View file

@ -23,6 +23,8 @@ from homeassistant import config_entries
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_DISCOVERY, CONF_DISCOVERY,
CONF_PASSWORD, CONF_PASSWORD,
@ -47,6 +49,7 @@ from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized
from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers import config_validation as cv, event, template
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send 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.frame import report
from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.helpers.typing import ConfigType, ServiceDataType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
@ -259,15 +262,22 @@ class MqttCommandTemplate:
def __init__( def __init__(
self, self,
command_template: template.Template | None, command_template: template.Template | None,
hass: HomeAssistant, *,
hass: HomeAssistant | None = None,
entity: Entity | None = None,
) -> None: ) -> None:
"""Instantiate a command template.""" """Instantiate a command template."""
self._attr_command_template = command_template self._attr_command_template = command_template
if command_template is None: if command_template is None:
return return
self._entity = entity
command_template.hass = hass command_template.hass = hass
if entity:
command_template.hass = entity.hass
@callback @callback
def async_render( def async_render(
self, self,
@ -295,6 +305,9 @@ class MqttCommandTemplate:
return value return value
values = {"value": 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: if variables is not None:
values.update(variables) values.update(variables)
return _convert_outgoing_payload( return _convert_outgoing_payload(
@ -613,7 +626,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if payload_template is not None: if payload_template is not None:
try: try:
payload = MqttCommandTemplate( payload = MqttCommandTemplate(
template.Template(payload_template), hass template.Template(payload_template), hass=hass
).async_render() ).async_render()
except (template.jinja2.TemplateError, TemplateError) as exc: except (template.jinja2.TemplateError, TemplateError) as exc:
_LOGGER.error( _LOGGER.error(

View file

@ -169,7 +169,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
if value_template is not None: if value_template is not None:
value_template.hass = self.hass value_template.hass = self.hass
self._command_template = MqttCommandTemplate( self._command_template = MqttCommandTemplate(
self._config[CONF_COMMAND_TEMPLATE], self.hass self._config[CONF_COMMAND_TEMPLATE], entity=self
).async_render ).async_render
async def _subscribe_topics(self): async def _subscribe_topics(self):

View file

@ -389,7 +389,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
command_templates = {} command_templates = {}
for key in COMMAND_TEMPLATE_KEYS: for key in COMMAND_TEMPLATE_KEYS:
command_templates[key] = MqttCommandTemplate( command_templates[key] = MqttCommandTemplate(
config.get(key), self.hass config.get(key), entity=self
).async_render ).async_render
self._command_templates = command_templates self._command_templates = command_templates

View file

@ -308,7 +308,7 @@ class MqttCover(MqttEntity, CoverEntity):
value_template.hass = self.hass value_template.hass = self.hass
self._set_position_template = MqttCommandTemplate( 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 ).async_render
get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE) get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE)
@ -316,7 +316,7 @@ class MqttCover(MqttEntity, CoverEntity):
get_position_template.hass = self.hass get_position_template.hass = self.hass
self._set_tilt_template = MqttCommandTemplate( 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 ).async_render
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)

View file

@ -352,7 +352,7 @@ class MqttFan(MqttEntity, FanEntity):
for key, tpl in self._command_templates.items(): for key, tpl in self._command_templates.items():
self._command_templates[key] = MqttCommandTemplate( self._command_templates[key] = MqttCommandTemplate(
tpl, self.hass tpl, entity=self
).async_render ).async_render
for key, tpl in self._value_templates.items(): for key, tpl in self._value_templates.items():

View file

@ -258,7 +258,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
for key, tpl in self._command_templates.items(): for key, tpl in self._command_templates.items():
self._command_templates[key] = MqttCommandTemplate( self._command_templates[key] = MqttCommandTemplate(
tpl, self.hass tpl, entity=self
).async_render ).async_render
for key, tpl in self._value_templates.items(): for key, tpl in self._value_templates.items():

View file

@ -51,7 +51,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .. import subscription from .. import MqttCommandTemplate, subscription
from ... import mqtt from ... import mqtt
from ..const import ( from ..const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
@ -336,9 +336,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
for key in COMMAND_TEMPLATE_KEYS: for key in COMMAND_TEMPLATE_KEYS:
command_templates[key] = None command_templates[key] = None
for key in COMMAND_TEMPLATE_KEYS & config.keys(): for key in COMMAND_TEMPLATE_KEYS & config.keys():
tpl = config[key] command_templates[key] = MqttCommandTemplate(
command_templates[key] = tpl.async_render config[key], entity=self
tpl.hass = self.hass ).async_render
self._command_templates = command_templates self._command_templates = command_templates
optimistic = config[CONF_OPTIMISTIC] optimistic = config[CONF_OPTIMISTIC]
@ -851,7 +851,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
keys.append("white") keys.append("white")
elif color_mode == COLOR_MODE_RGBWW: elif color_mode == COLOR_MODE_RGBWW:
keys.extend(["cold_white", "warm_white"]) keys.extend(["cold_white", "warm_white"])
rgb_color_str = tpl(zip(keys, color)) rgb_color_str = tpl(variables=zip(keys, color))
else: else:
rgb_color_str = ",".join(str(channel) for channel in color) rgb_color_str = ",".join(str(channel) for channel in color)
return rgb_color_str return rgb_color_str
@ -1017,9 +1017,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None
): ):
color_temp = int(kwargs[ATTR_COLOR_TEMP]) color_temp = int(kwargs[ATTR_COLOR_TEMP])
tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] if tpl := self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE]:
if tpl: color_temp = tpl(variables={"value": color_temp})
color_temp = tpl({"value": color_temp})
await publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) await publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp)
should_update |= set_optimistic( should_update |= set_optimistic(

View file

@ -155,7 +155,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity):
self._templates = { self._templates = {
CONF_COMMAND_TEMPLATE: MqttCommandTemplate( CONF_COMMAND_TEMPLATE: MqttCommandTemplate(
config.get(CONF_COMMAND_TEMPLATE), self.hass config.get(CONF_COMMAND_TEMPLATE), entity=self
).async_render, ).async_render,
CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE),
} }

View file

@ -121,7 +121,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
self._templates = { self._templates = {
CONF_COMMAND_TEMPLATE: MqttCommandTemplate( CONF_COMMAND_TEMPLATE: MqttCommandTemplate(
config.get(CONF_COMMAND_TEMPLATE), self.hass config.get(CONF_COMMAND_TEMPLATE), entity=self
).async_render, ).async_render,
CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE),
} }

View file

@ -12,10 +12,12 @@ from homeassistant.components import mqtt, websocket_api
from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
ATTR_ASSUMED_STATE,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
import homeassistant.core as ha
from homeassistant.core import CoreState, callback from homeassistant.core import CoreState, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, template 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): async def test_convert_outgoing_payload(hass):
"""Test the converting of outgoing MQTT payloads without template.""" """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 command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef"
assert ( assert (
@ -179,16 +181,63 @@ async def test_command_template_value(hass):
variables = {"id": 1234, "some_var": "beer"} variables = {"id": 1234, "some_var": "beer"}
# test rendering value # test rendering value
tpl = template.Template("{{ value + 1 }}", hass) tpl = template.Template("{{ value + 1 }}", hass=hass)
cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass)
assert cmd_tpl.async_render(4321) == "4322" assert cmd_tpl.async_render(4321) == "4322"
# test variables at rendering # test variables at rendering
tpl = template.Template("{{ some_var }}", hass) tpl = template.Template("{{ some_var }}", hass=hass)
cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass)
assert cmd_tpl.async_render(None, variables=variables) == "beer" 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): async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock):
"""Test the service call if topic is missing.""" """Test the service call if topic is missing."""
with pytest.raises(vol.Invalid): with pytest.raises(vol.Invalid):