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.
895 lines
32 KiB
Python
895 lines
32 KiB
Python
"""Support for MQTT climate devices."""
|
|
import functools
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components import climate
|
|
from homeassistant.components.climate import (
|
|
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA,
|
|
ClimateEntity,
|
|
)
|
|
from homeassistant.components.climate.const import (
|
|
ATTR_HVAC_MODE,
|
|
ATTR_TARGET_TEMP_HIGH,
|
|
ATTR_TARGET_TEMP_LOW,
|
|
DEFAULT_MAX_TEMP,
|
|
DEFAULT_MIN_TEMP,
|
|
FAN_AUTO,
|
|
FAN_HIGH,
|
|
FAN_LOW,
|
|
FAN_MEDIUM,
|
|
HVAC_MODE_AUTO,
|
|
HVAC_MODE_COOL,
|
|
HVAC_MODE_DRY,
|
|
HVAC_MODE_FAN_ONLY,
|
|
HVAC_MODE_HEAT,
|
|
HVAC_MODE_OFF,
|
|
PRESET_AWAY,
|
|
PRESET_NONE,
|
|
SUPPORT_AUX_HEAT,
|
|
SUPPORT_FAN_MODE,
|
|
SUPPORT_PRESET_MODE,
|
|
SUPPORT_SWING_MODE,
|
|
SUPPORT_TARGET_TEMPERATURE,
|
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_TEMPERATURE,
|
|
CONF_DEVICE,
|
|
CONF_NAME,
|
|
CONF_TEMPERATURE_UNIT,
|
|
CONF_UNIQUE_ID,
|
|
CONF_VALUE_TEMPLATE,
|
|
PRECISION_HALVES,
|
|
PRECISION_TENTHS,
|
|
PRECISION_WHOLE,
|
|
STATE_ON,
|
|
)
|
|
from homeassistant.core import callback
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.reload import async_setup_reload_service
|
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
|
|
|
from . import (
|
|
CONF_QOS,
|
|
CONF_RETAIN,
|
|
DOMAIN,
|
|
MQTT_BASE_PLATFORM_SCHEMA,
|
|
PLATFORMS,
|
|
subscription,
|
|
)
|
|
from .. import mqtt
|
|
from .debug_info import log_messages
|
|
from .mixins import (
|
|
MQTT_AVAILABILITY_SCHEMA,
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
|
MQTT_JSON_ATTRS_SCHEMA,
|
|
MqttEntity,
|
|
async_setup_entry_helper,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_NAME = "MQTT HVAC"
|
|
|
|
CONF_ACTION_TEMPLATE = "action_template"
|
|
CONF_ACTION_TOPIC = "action_topic"
|
|
CONF_AUX_COMMAND_TOPIC = "aux_command_topic"
|
|
CONF_AUX_STATE_TEMPLATE = "aux_state_template"
|
|
CONF_AUX_STATE_TOPIC = "aux_state_topic"
|
|
CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"
|
|
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"
|
|
CONF_MODE_STATE_TOPIC = "mode_state_topic"
|
|
CONF_PAYLOAD_OFF = "payload_off"
|
|
CONF_PAYLOAD_ON = "payload_on"
|
|
CONF_POWER_COMMAND_TOPIC = "power_command_topic"
|
|
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"
|
|
CONF_TEMP_STATE_TEMPLATE = "temperature_state_template"
|
|
CONF_TEMP_STATE_TOPIC = "temperature_state_topic"
|
|
CONF_TEMP_INITIAL = "initial"
|
|
CONF_TEMP_MAX = "max_temp"
|
|
CONF_TEMP_MIN = "min_temp"
|
|
CONF_TEMP_STEP = "temp_step"
|
|
|
|
VALUE_TEMPLATE_KEYS = (
|
|
CONF_AUX_STATE_TEMPLATE,
|
|
CONF_AWAY_MODE_STATE_TEMPLATE,
|
|
CONF_CURRENT_TEMP_TEMPLATE,
|
|
CONF_FAN_MODE_STATE_TEMPLATE,
|
|
CONF_HOLD_STATE_TEMPLATE,
|
|
CONF_MODE_STATE_TEMPLATE,
|
|
CONF_POWER_STATE_TEMPLATE,
|
|
CONF_ACTION_TEMPLATE,
|
|
CONF_SWING_MODE_STATE_TEMPLATE,
|
|
CONF_TEMP_HIGH_STATE_TEMPLATE,
|
|
CONF_TEMP_LOW_STATE_TEMPLATE,
|
|
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,
|
|
CONF_AWAY_MODE_COMMAND_TOPIC,
|
|
CONF_AWAY_MODE_STATE_TOPIC,
|
|
CONF_CURRENT_TEMP_TOPIC,
|
|
CONF_FAN_MODE_COMMAND_TOPIC,
|
|
CONF_FAN_MODE_STATE_TOPIC,
|
|
CONF_HOLD_COMMAND_TOPIC,
|
|
CONF_HOLD_STATE_TOPIC,
|
|
CONF_MODE_COMMAND_TOPIC,
|
|
CONF_MODE_STATE_TOPIC,
|
|
CONF_POWER_COMMAND_TOPIC,
|
|
CONF_POWER_STATE_TOPIC,
|
|
CONF_ACTION_TOPIC,
|
|
CONF_SWING_MODE_COMMAND_TOPIC,
|
|
CONF_SWING_MODE_STATE_TOPIC,
|
|
CONF_TEMP_COMMAND_TOPIC,
|
|
CONF_TEMP_HIGH_COMMAND_TOPIC,
|
|
CONF_TEMP_HIGH_STATE_TOPIC,
|
|
CONF_TEMP_LOW_COMMAND_TOPIC,
|
|
CONF_TEMP_LOW_STATE_TOPIC,
|
|
CONF_TEMP_STATE_TOPIC,
|
|
)
|
|
|
|
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
|
PLATFORM_SCHEMA = (
|
|
SCHEMA_BASE.extend(
|
|
{
|
|
vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
|
vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
|
vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
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,
|
|
default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
|
|
): 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,
|
|
default=[
|
|
HVAC_MODE_AUTO,
|
|
HVAC_MODE_OFF,
|
|
HVAC_MODE_COOL,
|
|
HVAC_MODE_HEAT,
|
|
HVAC_MODE_DRY,
|
|
HVAC_MODE_FAN_ONLY,
|
|
],
|
|
): cv.ensure_list,
|
|
vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
|
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
|
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
|
vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
vol.Optional(CONF_PRECISION): vol.In(
|
|
[PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
|
|
),
|
|
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
|
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]
|
|
): cv.ensure_list,
|
|
vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int,
|
|
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,
|
|
vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template,
|
|
vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
|
vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
|
|
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
|
}
|
|
)
|
|
.extend(MQTT_AVAILABILITY_SCHEMA.schema)
|
|
.extend(MQTT_JSON_ATTRS_SCHEMA.schema)
|
|
)
|
|
|
|
|
|
async def async_setup_platform(
|
|
hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None
|
|
):
|
|
"""Set up MQTT climate device through configuration.yaml."""
|
|
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
|
await _async_setup_entity(hass, config, async_add_entities)
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
"""Set up MQTT climate device dynamically through MQTT discovery."""
|
|
|
|
setup = functools.partial(
|
|
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
|
)
|
|
await async_setup_entry_helper(hass, climate.DOMAIN, setup, PLATFORM_SCHEMA)
|
|
|
|
|
|
async def _async_setup_entity(
|
|
hass, async_add_entities, config, config_entry=None, discovery_data=None
|
|
):
|
|
"""Set up the MQTT climate devices."""
|
|
async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)])
|
|
|
|
|
|
class MqttClimate(MqttEntity, ClimateEntity):
|
|
"""Representation of an MQTT climate device."""
|
|
|
|
def __init__(self, hass, config, config_entry, discovery_data):
|
|
"""Initialize the climate device."""
|
|
self._action = None
|
|
self._aux = False
|
|
self._away = False
|
|
self._current_fan_mode = None
|
|
self._current_operation = None
|
|
self._current_swing_mode = None
|
|
self._current_temp = None
|
|
self._hold = None
|
|
self._target_temp = None
|
|
self._target_temp_high = None
|
|
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)
|
|
|
|
@staticmethod
|
|
def config_schema():
|
|
"""Return the config schema."""
|
|
return PLATFORM_SCHEMA
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Handle being added to Home Assistant."""
|
|
await super().async_added_to_hass()
|
|
await self._subscribe_topics()
|
|
|
|
def _setup_from_config(self, config):
|
|
"""(Re)Setup the entity."""
|
|
self._config = config
|
|
self._topic = {key: config.get(key) for key in TOPIC_KEYS}
|
|
|
|
# set to None in non-optimistic mode
|
|
self._target_temp = (
|
|
self._current_fan_mode
|
|
) = self._current_operation = self._current_swing_mode = None
|
|
self._target_temp_low = None
|
|
self._target_temp_high = None
|
|
|
|
if self._topic[CONF_TEMP_STATE_TOPIC] is None:
|
|
self._target_temp = config[CONF_TEMP_INITIAL]
|
|
if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None:
|
|
self._target_temp_low = config[CONF_TEMP_INITIAL]
|
|
if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None:
|
|
self._target_temp_high = config[CONF_TEMP_INITIAL]
|
|
|
|
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
|
self._current_fan_mode = FAN_LOW
|
|
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
|
self._current_swing_mode = HVAC_MODE_OFF
|
|
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
|
self._current_operation = HVAC_MODE_OFF
|
|
self._action = None
|
|
self._away = False
|
|
self._hold = None
|
|
self._aux = False
|
|
|
|
value_templates = {}
|
|
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 VALUE_TEMPLATE_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 = {}
|
|
qos = self._config[CONF_QOS]
|
|
|
|
def add_subscription(topics, topic, msg_callback):
|
|
if self._topic[topic] is not None:
|
|
topics[topic] = {
|
|
"topic": self._topic[topic],
|
|
"msg_callback": msg_callback,
|
|
"qos": qos,
|
|
}
|
|
|
|
def render_template(msg, template_name):
|
|
template = self._value_templates[template_name]
|
|
return template(msg.payload)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_action_received(msg):
|
|
"""Handle receiving action via MQTT."""
|
|
payload = render_template(msg, CONF_ACTION_TEMPLATE)
|
|
|
|
self._action = payload
|
|
self.async_write_ha_state()
|
|
|
|
add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received)
|
|
|
|
@callback
|
|
def handle_temperature_received(msg, template_name, attr):
|
|
"""Handle temperature coming via MQTT."""
|
|
payload = render_template(msg, template_name)
|
|
|
|
try:
|
|
setattr(self, attr, float(payload))
|
|
self.async_write_ha_state()
|
|
except ValueError:
|
|
_LOGGER.error("Could not parse temperature from %s", payload)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_current_temperature_received(msg):
|
|
"""Handle current temperature coming via MQTT."""
|
|
handle_temperature_received(
|
|
msg, CONF_CURRENT_TEMP_TEMPLATE, "_current_temp"
|
|
)
|
|
|
|
add_subscription(
|
|
topics, CONF_CURRENT_TEMP_TOPIC, handle_current_temperature_received
|
|
)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_target_temperature_received(msg):
|
|
"""Handle target temperature coming via MQTT."""
|
|
handle_temperature_received(msg, CONF_TEMP_STATE_TEMPLATE, "_target_temp")
|
|
|
|
add_subscription(
|
|
topics, CONF_TEMP_STATE_TOPIC, handle_target_temperature_received
|
|
)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_temperature_low_received(msg):
|
|
"""Handle target temperature low coming via MQTT."""
|
|
handle_temperature_received(
|
|
msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_target_temp_low"
|
|
)
|
|
|
|
add_subscription(
|
|
topics, CONF_TEMP_LOW_STATE_TOPIC, handle_temperature_low_received
|
|
)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_temperature_high_received(msg):
|
|
"""Handle target temperature high coming via MQTT."""
|
|
handle_temperature_received(
|
|
msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_target_temp_high"
|
|
)
|
|
|
|
add_subscription(
|
|
topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received
|
|
)
|
|
|
|
@callback
|
|
def handle_mode_received(msg, template_name, attr, mode_list):
|
|
"""Handle receiving listed mode via MQTT."""
|
|
payload = render_template(msg, template_name)
|
|
|
|
if payload not in self._config[mode_list]:
|
|
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
|
else:
|
|
setattr(self, attr, payload)
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_current_mode_received(msg):
|
|
"""Handle receiving mode via MQTT."""
|
|
handle_mode_received(
|
|
msg, CONF_MODE_STATE_TEMPLATE, "_current_operation", CONF_MODE_LIST
|
|
)
|
|
|
|
add_subscription(topics, CONF_MODE_STATE_TOPIC, handle_current_mode_received)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_fan_mode_received(msg):
|
|
"""Handle receiving fan mode via MQTT."""
|
|
handle_mode_received(
|
|
msg,
|
|
CONF_FAN_MODE_STATE_TEMPLATE,
|
|
"_current_fan_mode",
|
|
CONF_FAN_MODE_LIST,
|
|
)
|
|
|
|
add_subscription(topics, CONF_FAN_MODE_STATE_TOPIC, handle_fan_mode_received)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_swing_mode_received(msg):
|
|
"""Handle receiving swing mode via MQTT."""
|
|
handle_mode_received(
|
|
msg,
|
|
CONF_SWING_MODE_STATE_TEMPLATE,
|
|
"_current_swing_mode",
|
|
CONF_SWING_MODE_LIST,
|
|
)
|
|
|
|
add_subscription(
|
|
topics, CONF_SWING_MODE_STATE_TOPIC, handle_swing_mode_received
|
|
)
|
|
|
|
@callback
|
|
def handle_onoff_mode_received(msg, template_name, attr):
|
|
"""Handle receiving on/off mode via MQTT."""
|
|
payload = render_template(msg, template_name)
|
|
payload_on = self._config[CONF_PAYLOAD_ON]
|
|
payload_off = self._config[CONF_PAYLOAD_OFF]
|
|
|
|
if payload == "True":
|
|
payload = payload_on
|
|
elif payload == "False":
|
|
payload = payload_off
|
|
|
|
if payload == payload_on:
|
|
setattr(self, attr, True)
|
|
elif payload == payload_off:
|
|
setattr(self, attr, False)
|
|
else:
|
|
_LOGGER.error("Invalid %s mode: %s", attr, payload)
|
|
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_away_mode_received(msg):
|
|
"""Handle receiving away mode via MQTT."""
|
|
handle_onoff_mode_received(msg, CONF_AWAY_MODE_STATE_TEMPLATE, "_away")
|
|
|
|
add_subscription(topics, CONF_AWAY_MODE_STATE_TOPIC, handle_away_mode_received)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_aux_mode_received(msg):
|
|
"""Handle receiving aux mode via MQTT."""
|
|
handle_onoff_mode_received(msg, CONF_AUX_STATE_TEMPLATE, "_aux")
|
|
|
|
add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received)
|
|
|
|
@callback
|
|
@log_messages(self.hass, self.entity_id)
|
|
def handle_hold_mode_received(msg):
|
|
"""Handle receiving hold mode via MQTT."""
|
|
payload = render_template(msg, CONF_HOLD_STATE_TEMPLATE)
|
|
|
|
if payload == "off":
|
|
payload = None
|
|
|
|
self._hold = payload
|
|
self.async_write_ha_state()
|
|
|
|
add_subscription(topics, CONF_HOLD_STATE_TOPIC, handle_hold_mode_received)
|
|
|
|
self._sub_state = await subscription.async_subscribe_topics(
|
|
self.hass, self._sub_state, topics
|
|
)
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the climate device."""
|
|
return self._config[CONF_NAME]
|
|
|
|
@property
|
|
def temperature_unit(self):
|
|
"""Return the unit of measurement."""
|
|
if self._config.get(CONF_TEMPERATURE_UNIT):
|
|
return self._config.get(CONF_TEMPERATURE_UNIT)
|
|
return self.hass.config.units.temperature_unit
|
|
|
|
@property
|
|
def current_temperature(self):
|
|
"""Return the current temperature."""
|
|
return self._current_temp
|
|
|
|
@property
|
|
def target_temperature(self):
|
|
"""Return the temperature we try to reach."""
|
|
return self._target_temp
|
|
|
|
@property
|
|
def target_temperature_low(self):
|
|
"""Return the low target temperature we try to reach."""
|
|
return self._target_temp_low
|
|
|
|
@property
|
|
def target_temperature_high(self):
|
|
"""Return the high target temperature we try to reach."""
|
|
return self._target_temp_high
|
|
|
|
@property
|
|
def hvac_action(self):
|
|
"""Return the current running hvac operation if supported."""
|
|
return self._action
|
|
|
|
@property
|
|
def hvac_mode(self):
|
|
"""Return current operation ie. heat, cool, idle."""
|
|
return self._current_operation
|
|
|
|
@property
|
|
def hvac_modes(self):
|
|
"""Return the list of available operation modes."""
|
|
return self._config[CONF_MODE_LIST]
|
|
|
|
@property
|
|
def target_temperature_step(self):
|
|
"""Return the supported step of target temperature."""
|
|
return self._config[CONF_TEMP_STEP]
|
|
|
|
@property
|
|
def preset_mode(self):
|
|
"""Return preset mode."""
|
|
if self._hold:
|
|
return self._hold
|
|
if self._away:
|
|
return PRESET_AWAY
|
|
return PRESET_NONE
|
|
|
|
@property
|
|
def preset_modes(self):
|
|
"""Return preset modes."""
|
|
presets = []
|
|
|
|
if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None
|
|
):
|
|
presets.append(PRESET_AWAY)
|
|
|
|
presets.extend(self._config[CONF_HOLD_LIST])
|
|
|
|
if presets:
|
|
presets.insert(0, PRESET_NONE)
|
|
|
|
return presets
|
|
|
|
@property
|
|
def is_aux_heat(self):
|
|
"""Return true if away mode is on."""
|
|
return self._aux
|
|
|
|
@property
|
|
def fan_mode(self):
|
|
"""Return the fan setting."""
|
|
return self._current_fan_mode
|
|
|
|
@property
|
|
def fan_modes(self):
|
|
"""Return the list of available fan modes."""
|
|
return self._config[CONF_FAN_MODE_LIST]
|
|
|
|
def _publish(self, topic, payload):
|
|
if self._topic[topic] is not None:
|
|
mqtt.async_publish(
|
|
self.hass,
|
|
self._topic[topic],
|
|
payload,
|
|
self._config[CONF_QOS],
|
|
self._config[CONF_RETAIN],
|
|
)
|
|
|
|
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
|
|
setattr(self, attr, temp)
|
|
|
|
if (
|
|
self._config[CONF_SEND_IF_OFF]
|
|
or self._current_operation != HVAC_MODE_OFF
|
|
):
|
|
payload = self._command_templates[cmnd_template](temp)
|
|
self._publish(cmnd_topic, payload)
|
|
|
|
async def async_set_temperature(self, **kwargs):
|
|
"""Set new target temperatures."""
|
|
if kwargs.get(ATTR_HVAC_MODE) is not None:
|
|
operation_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
await self.async_set_hvac_mode(operation_mode)
|
|
|
|
self._set_temperature(
|
|
kwargs.get(ATTR_TEMPERATURE),
|
|
CONF_TEMP_COMMAND_TOPIC,
|
|
CONF_TEMP_COMMAND_TEMPLATE,
|
|
CONF_TEMP_STATE_TOPIC,
|
|
"_target_temp",
|
|
)
|
|
|
|
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",
|
|
)
|
|
|
|
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",
|
|
)
|
|
|
|
# Always optimistic?
|
|
self.async_write_ha_state()
|
|
|
|
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:
|
|
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
|
|
self.async_write_ha_state()
|
|
|
|
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:
|
|
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
|
|
self.async_write_ha_state()
|
|
|
|
async def async_set_hvac_mode(self, hvac_mode) -> None:
|
|
"""Set new operation mode."""
|
|
if self._current_operation == HVAC_MODE_OFF and hvac_mode != HVAC_MODE_OFF:
|
|
self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON])
|
|
elif self._current_operation != HVAC_MODE_OFF and hvac_mode == HVAC_MODE_OFF:
|
|
self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF])
|
|
|
|
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
|
|
self.async_write_ha_state()
|
|
|
|
@property
|
|
def swing_mode(self):
|
|
"""Return the swing setting."""
|
|
return self._current_swing_mode
|
|
|
|
@property
|
|
def swing_modes(self):
|
|
"""List of available swing modes."""
|
|
return self._config[CONF_SWING_MODE_LIST]
|
|
|
|
async def async_set_preset_mode(self, preset_mode):
|
|
"""Set a preset mode."""
|
|
if preset_mode == self.preset_mode:
|
|
return
|
|
|
|
# Track if we should optimistic update the state
|
|
optimistic_update = False
|
|
|
|
if self._away:
|
|
optimistic_update = optimistic_update or self._set_away_mode(False)
|
|
elif preset_mode == PRESET_AWAY:
|
|
if self._hold:
|
|
self._set_hold_mode(None)
|
|
optimistic_update = optimistic_update or self._set_away_mode(True)
|
|
else:
|
|
hold_mode = preset_mode
|
|
if preset_mode == PRESET_NONE:
|
|
hold_mode = None
|
|
optimistic_update = optimistic_update or self._set_hold_mode(hold_mode)
|
|
|
|
if optimistic_update:
|
|
self.async_write_ha_state()
|
|
|
|
def _set_away_mode(self, state):
|
|
"""Set away mode.
|
|
|
|
Returns if we should optimistically write the state.
|
|
"""
|
|
self._publish(
|
|
CONF_AWAY_MODE_COMMAND_TOPIC,
|
|
self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF],
|
|
)
|
|
|
|
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
|
|
return False
|
|
|
|
self._away = state
|
|
return True
|
|
|
|
def _set_hold_mode(self, hold_mode):
|
|
"""Set hold mode.
|
|
|
|
Returns if we should optimistically write the state.
|
|
"""
|
|
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
|
|
|
|
self._hold = hold_mode
|
|
return True
|
|
|
|
def _set_aux_heat(self, state):
|
|
self._publish(
|
|
CONF_AUX_COMMAND_TOPIC,
|
|
self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF],
|
|
)
|
|
|
|
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
|
self._aux = state
|
|
self.async_write_ha_state()
|
|
|
|
async def async_turn_aux_heat_on(self):
|
|
"""Turn auxiliary heater on."""
|
|
self._set_aux_heat(True)
|
|
|
|
async def async_turn_aux_heat_off(self):
|
|
"""Turn auxiliary heater off."""
|
|
self._set_aux_heat(False)
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Return the list of supported features."""
|
|
support = 0
|
|
|
|
if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_TEMP_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_TARGET_TEMPERATURE
|
|
|
|
if (self._topic[CONF_TEMP_LOW_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_TEMP_LOW_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
|
|
|
if (self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_TEMP_HIGH_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
|
|
|
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_FAN_MODE
|
|
|
|
if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_SWING_MODE
|
|
|
|
if (
|
|
(self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None)
|
|
or (self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None)
|
|
or (self._topic[CONF_HOLD_STATE_TOPIC] is not None)
|
|
or (self._topic[CONF_HOLD_COMMAND_TOPIC] is not None)
|
|
):
|
|
support |= SUPPORT_PRESET_MODE
|
|
|
|
if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or (
|
|
self._topic[CONF_AUX_COMMAND_TOPIC] is not None
|
|
):
|
|
support |= SUPPORT_AUX_HEAT
|
|
|
|
return support
|
|
|
|
@property
|
|
def min_temp(self):
|
|
"""Return the minimum temperature."""
|
|
return self._config[CONF_TEMP_MIN]
|
|
|
|
@property
|
|
def max_temp(self):
|
|
"""Return the maximum temperature."""
|
|
return self._config[CONF_TEMP_MAX]
|
|
|
|
@property
|
|
def precision(self):
|
|
"""Return the precision of the system."""
|
|
if self._config.get(CONF_PRECISION) is not None:
|
|
return self._config.get(CONF_PRECISION)
|
|
return super().precision
|