Added command templates for the mqtt climate component. (#44976)

This allows integrating with devices which require more complex payloads to be posted when updating their values.

Old feature request: https://github.com/home-assistant/core/issues/11496
There are numerous posts requesting this feature, example: https://community.home-assistant.io/t/need-help-with-value-template-for-mqtt-hvac/73395/68https://community.home-assistant.io/t/need-help-with-value-template-for-mqtt-hvac/73395/68

Command templates have been added for the following:
- fan_mode
- hold
- mode
- swing_mode
- temperature
- temperature high/low

This doesn't add templates for aux, away mode, power since these already accept custom payload_on/off (although they all share the same payload). It should be straightforward to add templates for them as well if needed.
This commit is contained in:
radovanbauer 2021-01-26 07:12:33 -08:00 committed by GitHub
parent b1c2cde40b
commit baab9b9a81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 14 deletions

View File

@ -55,11 +55,13 @@ ABBREVIATIONS = {
"fx_tpl": "effect_template",
"fx_val_tpl": "effect_value_template",
"exp_aft": "expire_after",
"fan_mode_cmd_tpl": "fan_mode_command_template",
"fan_mode_cmd_t": "fan_mode_command_topic",
"fan_mode_stat_tpl": "fan_mode_state_template",
"fan_mode_stat_t": "fan_mode_state_topic",
"frc_upd": "force_update",
"g_tpl": "green_template",
"hold_cmd_tpl": "hold_command_template",
"hold_cmd_t": "hold_command_topic",
"hold_stat_tpl": "hold_state_template",
"hold_stat_t": "hold_state_topic",
@ -75,6 +77,7 @@ ABBREVIATIONS = {
"min_mirs": "min_mireds",
"max_temp": "max_temp",
"min_temp": "min_temp",
"mode_cmd_tpl": "mode_command_template",
"mode_cmd_t": "mode_command_topic",
"mode_stat_tpl": "mode_state_template",
"mode_stat_t": "mode_state_topic",
@ -151,13 +154,17 @@ ABBREVIATIONS = {
"stat_val_tpl": "state_value_template",
"stype": "subtype",
"sup_feat": "supported_features",
"swing_mode_cmd_tpl": "swing_mode_command_template",
"swing_mode_cmd_t": "swing_mode_command_topic",
"swing_mode_stat_tpl": "swing_mode_state_template",
"swing_mode_stat_t": "swing_mode_state_topic",
"temp_cmd_tpl": "temperature_command_template",
"temp_cmd_t": "temperature_command_topic",
"temp_hi_cmd_tpl": "temperature_high_command_template",
"temp_hi_cmd_t": "temperature_high_command_topic",
"temp_hi_stat_tpl": "temperature_high_state_template",
"temp_hi_stat_t": "temperature_high_state_topic",
"temp_lo_cmd_tpl": "temperature_low_command_template",
"temp_lo_cmd_t": "temperature_low_command_topic",
"temp_lo_stat_tpl": "temperature_low_state_template",
"temp_lo_stat_t": "temperature_low_state_topic",

View File

@ -83,14 +83,17 @@ CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"
CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"
CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template"
CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic"
CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"
CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic"
CONF_FAN_MODE_LIST = "fan_modes"
CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template"
CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic"
CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template"
CONF_HOLD_COMMAND_TOPIC = "hold_command_topic"
CONF_HOLD_STATE_TEMPLATE = "hold_state_template"
CONF_HOLD_STATE_TOPIC = "hold_state_topic"
CONF_HOLD_LIST = "hold_modes"
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
CONF_MODE_COMMAND_TOPIC = "mode_command_topic"
CONF_MODE_LIST = "modes"
CONF_MODE_STATE_TEMPLATE = "mode_state_template"
@ -102,14 +105,18 @@ CONF_POWER_STATE_TEMPLATE = "power_state_template"
CONF_POWER_STATE_TOPIC = "power_state_topic"
CONF_PRECISION = "precision"
CONF_SEND_IF_OFF = "send_if_off"
CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template"
CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic"
CONF_SWING_MODE_LIST = "swing_modes"
CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template"
CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic"
CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template"
CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic"
CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"
CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic"
CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template"
CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic"
CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template"
CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic"
CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template"
CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic"
@ -120,7 +127,7 @@ CONF_TEMP_MAX = "max_temp"
CONF_TEMP_MIN = "min_temp"
CONF_TEMP_STEP = "temp_step"
TEMPLATE_KEYS = (
VALUE_TEMPLATE_KEYS = (
CONF_AUX_STATE_TEMPLATE,
CONF_AWAY_MODE_STATE_TEMPLATE,
CONF_CURRENT_TEMP_TEMPLATE,
@ -135,6 +142,16 @@ TEMPLATE_KEYS = (
CONF_TEMP_STATE_TEMPLATE,
)
COMMAND_TEMPLATE_KEYS = {
CONF_FAN_MODE_COMMAND_TEMPLATE,
CONF_HOLD_COMMAND_TEMPLATE,
CONF_MODE_COMMAND_TEMPLATE,
CONF_SWING_MODE_COMMAND_TEMPLATE,
CONF_TEMP_COMMAND_TEMPLATE,
CONF_TEMP_HIGH_COMMAND_TEMPLATE,
CONF_TEMP_LOW_COMMAND_TEMPLATE,
}
TOPIC_KEYS = (
CONF_AUX_COMMAND_TOPIC,
CONF_AUX_STATE_TOPIC,
@ -173,6 +190,7 @@ PLATFORM_SCHEMA = (
vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
CONF_FAN_MODE_LIST,
@ -180,10 +198,12 @@ PLATFORM_SCHEMA = (
): cv.ensure_list,
vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
CONF_MODE_LIST,
@ -211,6 +231,7 @@ PLATFORM_SCHEMA = (
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF]
@ -221,10 +242,13 @@ PLATFORM_SCHEMA = (
vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic,
@ -282,6 +306,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._target_temp_low = None
self._topic = None
self._value_templates = None
self._command_templates = None
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@ -326,21 +351,30 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._aux = False
value_templates = {}
for key in TEMPLATE_KEYS:
for key in VALUE_TEMPLATE_KEYS:
value_templates[key] = lambda value: value
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 TEMPLATE_KEYS
for key in VALUE_TEMPLATE_KEYS
}
for key in TEMPLATE_KEYS & config.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
command_templates = {}
for key in COMMAND_TEMPLATE_KEYS:
command_templates[key] = lambda value: value
for key in COMMAND_TEMPLATE_KEYS & config.keys():
tpl = config[key]
command_templates[key] = tpl.async_render_with_possible_json_value
tpl.hass = self.hass
self._command_templates = command_templates
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
@ -633,7 +667,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._config[CONF_RETAIN],
)
def _set_temperature(self, temp, cmnd_topic, state_topic, attr):
def _set_temperature(self, temp, cmnd_topic, cmnd_template, state_topic, attr):
if temp is not None:
if self._topic[state_topic] is None:
# optimistic mode
@ -643,7 +677,8 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._config[CONF_SEND_IF_OFF]
or self._current_operation != HVAC_MODE_OFF
):
self._publish(cmnd_topic, temp)
payload = self._command_templates[cmnd_template](temp)
self._publish(cmnd_topic, payload)
async def async_set_temperature(self, **kwargs):
"""Set new target temperatures."""
@ -654,6 +689,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._set_temperature(
kwargs.get(ATTR_TEMPERATURE),
CONF_TEMP_COMMAND_TOPIC,
CONF_TEMP_COMMAND_TEMPLATE,
CONF_TEMP_STATE_TOPIC,
"_target_temp",
)
@ -661,6 +697,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._set_temperature(
kwargs.get(ATTR_TARGET_TEMP_LOW),
CONF_TEMP_LOW_COMMAND_TOPIC,
CONF_TEMP_LOW_COMMAND_TEMPLATE,
CONF_TEMP_LOW_STATE_TOPIC,
"_target_temp_low",
)
@ -668,6 +705,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._set_temperature(
kwargs.get(ATTR_TARGET_TEMP_HIGH),
CONF_TEMP_HIGH_COMMAND_TOPIC,
CONF_TEMP_HIGH_COMMAND_TEMPLATE,
CONF_TEMP_HIGH_STATE_TOPIC,
"_target_temp_high",
)
@ -678,7 +716,10 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
self._publish(CONF_SWING_MODE_COMMAND_TOPIC, swing_mode)
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](
swing_mode
)
self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload)
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = swing_mode
@ -687,7 +728,8 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
self._publish(CONF_FAN_MODE_COMMAND_TOPIC, fan_mode)
payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode)
self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload)
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = fan_mode
@ -700,7 +742,8 @@ class MqttClimate(MqttEntity, ClimateEntity):
elif self._current_operation != HVAC_MODE_OFF and hvac_mode == HVAC_MODE_OFF:
self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF])
self._publish(CONF_MODE_COMMAND_TOPIC, hvac_mode)
payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode)
self._publish(CONF_MODE_COMMAND_TOPIC, payload)
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = hvac_mode
@ -760,7 +803,10 @@ class MqttClimate(MqttEntity, ClimateEntity):
Returns if we should optimistically write the state.
"""
self._publish(CONF_HOLD_COMMAND_TOPIC, hold_mode or "off")
payload = self._command_templates[CONF_HOLD_COMMAND_TEMPLATE](
hold_mode or "off"
)
self._publish(CONF_HOLD_COMMAND_TOPIC, payload)
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
return False

View File

@ -644,8 +644,8 @@ async def test_custom_availability_payload(hass, mqtt_mock):
)
async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog):
"""Test setting of temperature high/low templates."""
async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog):
"""Test getting temperature high/low with templates."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["climate"]["temperature_low_state_topic"] = "temperature-state"
config["climate"]["temperature_high_state_topic"] = "temperature-state"
@ -678,8 +678,8 @@ async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, c
assert state.attributes.get("target_temp_high") == 1032
async def test_set_with_templates(hass, mqtt_mock, caplog):
"""Test setting of new fan mode in pessimistic mode."""
async def test_get_with_templates(hass, mqtt_mock, caplog):
"""Test getting various attributes with templates."""
config = copy.deepcopy(DEFAULT_CONFIG)
# By default, just unquote the JSON-strings
config["climate"]["value_template"] = "{{ value_json }}"
@ -782,6 +782,80 @@ async def test_set_with_templates(hass, mqtt_mock, caplog):
assert state.attributes.get("hvac_action") == "cool"
async def test_set_with_templates(hass, mqtt_mock, caplog):
"""Test setting various attributes with templates."""
config = copy.deepcopy(DEFAULT_CONFIG)
# Create simple templates
config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}"
config["climate"]["hold_command_template"] = "hold: {{ value }}"
config["climate"]["mode_command_template"] = "mode: {{ value }}"
config["climate"]["swing_mode_command_template"] = "swing_mode: {{ value }}"
config["climate"]["temperature_command_template"] = "temp: {{ value }}"
config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}"
config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
# Fan Mode
await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"fan-mode-topic", "fan_mode: high", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("fan_mode") == "high"
# Hold Mode
await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with("hold-topic", "hold: eco", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == PRESET_ECO
# Mode
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"mode-topic", "mode: cool", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "cool"
# Swing Mode
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"swing-mode-topic", "swing_mode: on", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
# Temperature
await common.async_set_temperature(hass, temperature=47, entity_id=ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"temperature-topic", "temp: 47.0", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("temperature") == 47
# Temperature Low/High
await common.async_set_temperature(
hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE
)
mqtt_mock.async_publish.assert_any_call(
"temperature-low-topic", "temp_lo: 20.0", 0, False
)
mqtt_mock.async_publish.assert_any_call(
"temperature-high-topic", "temp_hi: 23.0", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("target_temp_low") == 20
assert state.attributes.get("target_temp_high") == 23
async def test_min_temp_custom(hass, mqtt_mock):
"""Test a custom min temp."""
config = copy.deepcopy(DEFAULT_CONFIG)