Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 96 additions & 7 deletions homeassistant/components/climate/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability)
Expand All @@ -35,21 +35,30 @@

CONF_POWER_COMMAND_TOPIC = 'power_command_topic'
CONF_POWER_STATE_TOPIC = 'power_state_topic'
CONF_POWER_STATE_TEMPLATE = 'power_state_template'
CONF_MODE_COMMAND_TOPIC = 'mode_command_topic'
CONF_MODE_STATE_TOPIC = 'mode_state_topic'
CONF_MODE_STATE_TEMPLATE = 'mode_state_template'
CONF_TEMPERATURE_COMMAND_TOPIC = 'temperature_command_topic'
CONF_TEMPERATURE_STATE_TOPIC = 'temperature_state_topic'
CONF_TEMPERATURE_STATE_TEMPLATE = 'temperature_state_template'
CONF_FAN_MODE_COMMAND_TOPIC = 'fan_mode_command_topic'
CONF_FAN_MODE_STATE_TOPIC = 'fan_mode_state_topic'
CONF_FAN_MODE_STATE_TEMPLATE = 'fan_mode_state_template'
CONF_SWING_MODE_COMMAND_TOPIC = 'swing_mode_command_topic'
CONF_SWING_MODE_STATE_TOPIC = 'swing_mode_state_topic'
CONF_SWING_MODE_STATE_TEMPLATE = 'swing_mode_state_template'
CONF_AWAY_MODE_COMMAND_TOPIC = 'away_mode_command_topic'
CONF_AWAY_MODE_STATE_TOPIC = 'away_mode_state_topic'
CONF_AWAY_MODE_STATE_TEMPLATE = 'away_mode_state_template'
CONF_HOLD_COMMAND_TOPIC = 'hold_command_topic'
CONF_HOLD_STATE_TOPIC = 'hold_state_topic'
CONF_HOLD_STATE_TEMPLATE = 'hold_state_template'
CONF_AUX_COMMAND_TOPIC = 'aux_command_topic'
CONF_AUX_STATE_TOPIC = 'aux_state_topic'
CONF_AUX_STATE_TEMPLATE = 'aux_state_template'

CONF_CURRENT_TEMPERATURE_TEMPLATE = 'current_temperature_template'
CONF_CURRENT_TEMPERATURE_TOPIC = 'current_temperature_topic'

CONF_PAYLOAD_ON = 'payload_on'
Expand All @@ -71,6 +80,7 @@
vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,

vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_TEMPERATURE_STATE_TOPIC): mqtt.valid_subscribe_topic,
Expand All @@ -79,6 +89,18 @@
vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,

vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_CURRENT_TEMPERATURE_TEMPLATE): cv.template,

vol.Optional(CONF_CURRENT_TEMPERATURE_TOPIC):
mqtt.valid_subscribe_topic,
vol.Optional(CONF_FAN_MODE_LIST,
Expand All @@ -100,6 +122,26 @@
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT climate devices."""
template_keys = (
CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
CONF_TEMPERATURE_STATE_TEMPLATE,
CONF_FAN_MODE_STATE_TEMPLATE,
CONF_SWING_MODE_STATE_TEMPLATE,
CONF_AWAY_MODE_STATE_TEMPLATE,
CONF_HOLD_STATE_TEMPLATE,
CONF_AUX_STATE_TEMPLATE,
CONF_CURRENT_TEMPERATURE_TEMPLATE
)
value_templates = {}
if CONF_VALUE_TEMPLATE in config:
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
value_templates = {key: value_template for key in template_keys}
for key in template_keys & config.keys():
value_templates[key] = config.get(key)
value_templates[key].hass = hass

async_add_devices([
MqttClimate(
hass,
Expand All @@ -125,6 +167,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
CONF_CURRENT_TEMPERATURE_TOPIC
)
},
value_templates,
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_MODE_LIST),
Expand All @@ -145,18 +188,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class MqttClimate(MqttAvailability, ClimateDevice):
"""Representation of a demo climate device."""

def __init__(self, hass, name, topic, qos, retain, mode_list,
fan_mode_list, swing_mode_list, target_temperature, away,
hold, current_fan_mode, current_swing_mode,
current_operation, aux, send_if_off, payload_on,
payload_off, availability_topic, payload_available,
payload_not_available):
def __init__(self, hass, name, topic, value_templates, qos, retain,
mode_list, fan_mode_list, swing_mode_list,
target_temperature, away, hold, current_fan_mode,
current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic,
payload_available, payload_not_available):
"""Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self.hass = hass
self._name = name
self._topic = topic
self._value_templates = value_templates
self._qos = qos
self._retain = retain
self._target_temperature = target_temperature
Expand Down Expand Up @@ -184,6 +228,11 @@ def async_added_to_hass(self):
@callback
def handle_current_temp_received(topic, payload, qos):
"""Handle current temperature coming via MQTT."""
if CONF_CURRENT_TEMPERATURE_TEMPLATE in self._value_templates:
payload =\
self._value_templates[CONF_CURRENT_TEMPERATURE_TEMPLATE].\
async_render_with_possible_json_value(payload)

try:
self._current_temperature = float(payload)
self.async_schedule_update_ha_state()
Expand All @@ -198,6 +247,10 @@ def handle_current_temp_received(topic, payload, qos):
@callback
def handle_mode_received(topic, payload, qos):
"""Handle receiving mode via MQTT."""
if CONF_MODE_STATE_TEMPLATE in self._value_templates:
payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)

if payload not in self._operation_list:
_LOGGER.error("Invalid mode: %s", payload)
else:
Expand All @@ -212,6 +265,11 @@ def handle_mode_received(topic, payload, qos):
@callback
def handle_temperature_received(topic, payload, qos):
"""Handle target temperature coming via MQTT."""
if CONF_TEMPERATURE_STATE_TEMPLATE in self._value_templates:
payload = \
self._value_templates[CONF_TEMPERATURE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)

try:
self._target_temperature = float(payload)
self.async_schedule_update_ha_state()
Expand All @@ -226,6 +284,11 @@ def handle_temperature_received(topic, payload, qos):
@callback
def handle_fan_mode_received(topic, payload, qos):
"""Handle receiving fan mode via MQTT."""
if CONF_FAN_MODE_STATE_TEMPLATE in self._value_templates:
payload = \
self._value_templates[CONF_FAN_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)

if payload not in self._fan_list:
_LOGGER.error("Invalid fan mode: %s", payload)
else:
Expand All @@ -240,6 +303,11 @@ def handle_fan_mode_received(topic, payload, qos):
@callback
def handle_swing_mode_received(topic, payload, qos):
"""Handle receiving swing mode via MQTT."""
if CONF_SWING_MODE_STATE_TEMPLATE in self._value_templates:
payload = \
self._value_templates[CONF_SWING_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)

if payload not in self._swing_list:
_LOGGER.error("Invalid swing mode: %s", payload)
else:
Expand All @@ -254,6 +322,15 @@ def handle_swing_mode_received(topic, payload, qos):
@callback
def handle_away_mode_received(topic, payload, qos):
"""Handle receiving away mode via MQTT."""
if CONF_AWAY_MODE_STATE_TEMPLATE in self._value_templates:
payload = \
self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload == "True":
payload = self._payload_on
elif payload == "False":
payload = self._payload_off

if payload == self._payload_on:
self._away = True
elif payload == self._payload_off:
Expand All @@ -271,6 +348,14 @@ def handle_away_mode_received(topic, payload, qos):
@callback
def handle_aux_mode_received(topic, payload, qos):
"""Handle receiving aux mode via MQTT."""
if CONF_AUX_STATE_TEMPLATE in self._value_templates:
payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload == "True":
payload = self._payload_on
elif payload == "False":
payload = self._payload_off

if payload == self._payload_on:
self._aux = True
elif payload == self._payload_off:
Expand All @@ -288,6 +373,10 @@ def handle_aux_mode_received(topic, payload, qos):
@callback
def handle_hold_mode_received(topic, payload, qos):
"""Handle receiving hold mode via MQTT."""
if CONF_HOLD_STATE_TEMPLATE in self._value_templates:
payload = self._value_templates[CONF_HOLD_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)

self._hold = payload
self.async_schedule_update_ha_state()

Expand Down
81 changes: 81 additions & 0 deletions tests/components/climate/test_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,84 @@ def test_custom_availability_payload(self):

state = self.hass.states.get('climate.test')
self.assertEqual(STATE_UNAVAILABLE, state.state)

def test_set_with_templates(self):
"""Test setting of new fan mode in pessimistic mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
# By default, just unquote the JSON-strings
config['climate']['value_template'] = '{{ value_json }}'
# Something more complicated for hold mode
config['climate']['hold_state_template'] = \
'{{ value_json.attribute }}'
# Rendering to a bool for aux heat
config['climate']['aux_state_template'] = \
"{{ value == 'switchmeon' }}"

config['climate']['mode_state_topic'] = 'mode-state'
config['climate']['fan_mode_state_topic'] = 'fan-state'
config['climate']['swing_mode_state_topic'] = 'swing-state'
config['climate']['temperature_state_topic'] = 'temperature-state'
config['climate']['away_mode_state_topic'] = 'away-state'
config['climate']['hold_state_topic'] = 'hold-state'
config['climate']['aux_state_topic'] = 'aux-state'
config['climate']['current_temperature_topic'] = 'current-temperature'

assert setup_component(self.hass, climate.DOMAIN, config)

# Operation Mode
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("off", state.attributes.get('operation_mode'))
fire_mqtt_message(self.hass, 'mode-state', '"cool"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("cool", state.attributes.get('operation_mode'))

# Fan Mode
self.assertEqual("low", state.attributes.get('fan_mode'))
fire_mqtt_message(self.hass, 'fan-state', '"high"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('high', state.attributes.get('fan_mode'))

# Swing Mode
self.assertEqual("off", state.attributes.get('swing_mode'))
fire_mqtt_message(self.hass, 'swing-state', '"on"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("on", state.attributes.get('swing_mode'))

# Temperature
self.assertEqual(21, state.attributes.get('temperature'))
fire_mqtt_message(self.hass, 'temperature-state', '"1031"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(1031, state.attributes.get('temperature'))

# Away Mode
self.assertEqual('off', state.attributes.get('away_mode'))
fire_mqtt_message(self.hass, 'away-state', '"ON"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('away_mode'))

# Hold Mode
self.assertEqual(None, state.attributes.get('hold_mode'))
fire_mqtt_message(self.hass, 'hold-state', """
{ "attribute": "somemode" }
""")
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('somemode', state.attributes.get('hold_mode'))

# Aux mode
self.assertEqual('off', state.attributes.get('aux_heat'))
fire_mqtt_message(self.hass, 'aux-state', 'switchmeon')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('aux_heat'))

# Current temperature
fire_mqtt_message(self.hass, 'current-temperature', '"74656"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(74656, state.attributes.get('current_temperature'))