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.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(

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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():

View file

@ -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():

View file

@ -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(

View file

@ -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),
}

View file

@ -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),
}

View file

@ -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):