diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 9cfa84c6c1b0..4b209f6f364b 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -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", diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index b0d9ebd6ffab..15c7c916eeb5 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -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 diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 97657e9e3e68..546c112b1531 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -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)