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:
parent
7f9b7c7b0e
commit
457ce195dd
10 changed files with 84 additions and 23 deletions
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Add table
Reference in a new issue