* New climate platform with MQTT * Use STATE_OFF * Basic tests for climate.mqtt * lint * actualy collect coverage * First tests and fixes * Add possibility to receive temperature via MQTT * Require only either sensor or mqtt topic * Add mqtt publishing for away mode, hold mode and aux heat. * Use configurabe on/off payloads * Add pessimistic mode * Initialize aux and away with False instead of None * Remove Sensor * Use correct scheduling method * Move all methods to coroutines
This commit is contained in:
parent
80a15977ff
commit
29c40622d3
2 changed files with 903 additions and 0 deletions
483
homeassistant/components/climate/mqtt.py
Normal file
483
homeassistant/components/climate/mqtt.py
Normal file
|
@ -0,0 +1,483 @@
|
|||
"""
|
||||
Support for MQTT climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
|
||||
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
|
||||
ATTR_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
|
||||
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
DEFAULT_NAME = 'MQTT HVAC'
|
||||
|
||||
CONF_POWER_COMMAND_TOPIC = 'power_command_topic'
|
||||
CONF_POWER_STATE_TOPIC = 'power_state_topic'
|
||||
CONF_MODE_COMMAND_TOPIC = 'mode_command_topic'
|
||||
CONF_MODE_STATE_TOPIC = 'mode_state_topic'
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC = 'temperature_command_topic'
|
||||
CONF_TEMPERATURE_STATE_TOPIC = 'temperature_state_topic'
|
||||
CONF_FAN_MODE_COMMAND_TOPIC = 'fan_mode_command_topic'
|
||||
CONF_FAN_MODE_STATE_TOPIC = 'fan_mode_state_topic'
|
||||
CONF_SWING_MODE_COMMAND_TOPIC = 'swing_mode_command_topic'
|
||||
CONF_SWING_MODE_STATE_TOPIC = 'swing_mode_state_topic'
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC = 'away_mode_command_topic'
|
||||
CONF_AWAY_MODE_STATE_TOPIC = 'away_mode_state_topic'
|
||||
CONF_HOLD_COMMAND_TOPIC = 'hold_command_topic'
|
||||
CONF_HOLD_STATE_TOPIC = 'hold_state_topic'
|
||||
CONF_AUX_COMMAND_TOPIC = 'aux_command_topic'
|
||||
CONF_AUX_STATE_TOPIC = 'aux_state_topic'
|
||||
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC = 'current_temperature_topic'
|
||||
|
||||
CONF_PAYLOAD_ON = 'payload_on'
|
||||
CONF_PAYLOAD_OFF = 'payload_off'
|
||||
|
||||
CONF_FAN_MODE_LIST = 'fan_modes'
|
||||
CONF_MODE_LIST = 'modes'
|
||||
CONF_SWING_MODE_LIST = 'swing_modes'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_SEND_IF_OFF = 'send_if_off'
|
||||
|
||||
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
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,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
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_CURRENT_TEMPERATURE_TOPIC):
|
||||
mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_LIST,
|
||||
default=[STATE_AUTO, SPEED_LOW,
|
||||
SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
|
||||
vol.Optional(CONF_SWING_MODE_LIST,
|
||||
default=[STATE_ON, STATE_OFF]): cv.ensure_list,
|
||||
vol.Optional(CONF_MODE_LIST,
|
||||
default=[STATE_AUTO, STATE_OFF, STATE_COOL, STATE_HEAT,
|
||||
STATE_DRY, STATE_FAN_ONLY]): cv.ensure_list,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL, default=21): cv.positive_int,
|
||||
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT climate devices."""
|
||||
async_add_devices([
|
||||
MqttClimate(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_SWING_MODE_COMMAND_TOPIC,
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC,
|
||||
CONF_HOLD_COMMAND_TOPIC,
|
||||
CONF_AUX_COMMAND_TOPIC,
|
||||
CONF_POWER_STATE_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
CONF_AWAY_MODE_STATE_TOPIC,
|
||||
CONF_HOLD_STATE_TOPIC,
|
||||
CONF_AUX_STATE_TOPIC,
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC
|
||||
)
|
||||
},
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_MODE_LIST),
|
||||
config.get(CONF_FAN_MODE_LIST),
|
||||
config.get(CONF_SWING_MODE_LIST),
|
||||
config.get(CONF_INITIAL),
|
||||
False, None, SPEED_LOW,
|
||||
STATE_OFF, STATE_OFF, False,
|
||||
config.get(CONF_SEND_IF_OFF),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF))
|
||||
])
|
||||
|
||||
|
||||
class MqttClimate(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):
|
||||
"""Initialize the climate device."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._target_temperature = target_temperature
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = current_fan_mode
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = fan_mode_list
|
||||
self._operation_list = mode_list
|
||||
self._swing_list = swing_mode_list
|
||||
self._target_temperature_step = 1
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
@callback
|
||||
def handle_current_temp_received(topic, payload, qos):
|
||||
"""Handle current temperature coming via MQTT."""
|
||||
try:
|
||||
self._current_temperature = float(payload)
|
||||
self.async_schedule_update_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
handle_current_temp_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_mode_received(topic, payload, qos):
|
||||
"""Handle receiving mode via MQTT."""
|
||||
if payload not in self._operation_list:
|
||||
_LOGGER.error("Invalid mode: %s", payload)
|
||||
else:
|
||||
self._current_operation = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_MODE_STATE_TOPIC],
|
||||
handle_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_temperature_received(topic, payload, qos):
|
||||
"""Handle target temperature coming via MQTT."""
|
||||
try:
|
||||
self._target_temperature = float(payload)
|
||||
self.async_schedule_update_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
handle_temperature_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_fan_mode_received(topic, payload, qos):
|
||||
"""Handle receiving fan mode via MQTT."""
|
||||
if payload not in self._fan_list:
|
||||
_LOGGER.error("Invalid fan mode: %s", payload)
|
||||
else:
|
||||
self._current_fan_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
handle_fan_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_swing_mode_received(topic, payload, qos):
|
||||
"""Handle receiving swing mode via MQTT."""
|
||||
if payload not in self._swing_list:
|
||||
_LOGGER.error("Invalid swing mode: %s", payload)
|
||||
else:
|
||||
self._current_swing_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
handle_swing_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_away_mode_received(topic, payload, qos):
|
||||
"""Handle receiving away mode via MQTT."""
|
||||
if payload == self._payload_on:
|
||||
self._away = True
|
||||
elif payload == self._payload_off:
|
||||
self._away = False
|
||||
else:
|
||||
_LOGGER.error("Invalid away mode: %s", payload)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
handle_away_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_aux_mode_received(topic, payload, qos):
|
||||
"""Handle receiving aux mode via MQTT."""
|
||||
if payload == self._payload_on:
|
||||
self._aux = True
|
||||
elif payload == self._payload_off:
|
||||
self._aux = False
|
||||
else:
|
||||
_LOGGER.error("Invalid aux mode: %s", payload)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AUX_STATE_TOPIC],
|
||||
handle_aux_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_hold_mode_received(topic, payload, qos):
|
||||
"""Handle receiving hold mode via MQTT."""
|
||||
self._hold = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
handle_hold_mode_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._target_temperature_step
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self._away
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return hold mode setting."""
|
||||
return self._hold
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
if kwargs.get(ATTR_OPERATION_MODE) is not None:
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
yield from self.async_set_operation_mode(operation_mode)
|
||||
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
# optimistic mode
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC],
|
||||
kwargs.get(ATTR_TEMPERATURE), self._qos, self._retain)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new swing mode."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC],
|
||||
swing_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
self._current_swing_mode = swing_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
|
||||
fan, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = fan
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode) -> None:
|
||||
"""Set new operation mode."""
|
||||
if self._topic[CONF_POWER_COMMAND_TOPIC] is not None:
|
||||
if (self._current_operation == STATE_OFF and
|
||||
operation_mode != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
elif (self._current_operation != STATE_OFF and
|
||||
operation_mode == STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_MODE_COMMAND_TOPIC],
|
||||
operation_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = operation_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_hold_mode(self, hold):
|
||||
"""Update hold mode on."""
|
||||
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_HOLD_COMMAND_TOPIC],
|
||||
hold, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
|
||||
self._hold = hold
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = False
|
||||
self.async_schedule_update_ha_state()
|
420
tests/components/climate/test_mqtt.py
Normal file
420
tests/components/climate/test_mqtt.py
Normal file
|
@ -0,0 +1,420 @@
|
|||
"""The tests for the mqtt climate component."""
|
||||
import unittest
|
||||
import copy
|
||||
|
||||
from homeassistant.util.unit_system import (
|
||||
METRIC_SYSTEM
|
||||
)
|
||||
from homeassistant.setup import setup_component
|
||||
from homeassistant.components import climate
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
||||
from tests.common import (get_test_home_assistant, mock_mqtt_component,
|
||||
fire_mqtt_message, mock_component)
|
||||
|
||||
ENTITY_CLIMATE = 'climate.test'
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
'climate': {
|
||||
'platform': 'mqtt',
|
||||
'name': 'test',
|
||||
'mode_command_topic': 'mode-topic',
|
||||
'temperature_command_topic': 'temperature-topic',
|
||||
'fan_mode_command_topic': 'fan-mode-topic',
|
||||
'swing_mode_command_topic': 'swing-mode-topic',
|
||||
'away_mode_command_topic': 'away-mode-topic',
|
||||
'hold_command_topic': 'hold-topic',
|
||||
'aux_command_topic': 'aux-topic'
|
||||
}}
|
||||
|
||||
|
||||
class TestMQTTClimate(unittest.TestCase):
|
||||
"""Test the mqtt climate hvac."""
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.mock_publish = mock_mqtt_component(self.hass)
|
||||
self.hass.config.units = METRIC_SYSTEM
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_setup_params(self):
|
||||
"""Test the initial parameters."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(21, state.attributes.get('temperature'))
|
||||
self.assertEqual("low", state.attributes.get('fan_mode'))
|
||||
self.assertEqual("off", state.attributes.get('swing_mode'))
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
|
||||
def test_get_operation_modes(self):
|
||||
"""Test that the operation list returns the correct modes."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
modes = state.attributes.get('operation_list')
|
||||
self.assertEqual([
|
||||
climate.STATE_AUTO, STATE_OFF, climate.STATE_COOL,
|
||||
climate.STATE_HEAT, climate.STATE_DRY, climate.STATE_FAN_ONLY
|
||||
], modes)
|
||||
|
||||
def test_set_operation_bad_attr_and_state(self):
|
||||
"""Test setting operation mode without required attribute.
|
||||
|
||||
Also check the state.
|
||||
"""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("off", state.state)
|
||||
climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("off", state.state)
|
||||
|
||||
def test_set_operation(self):
|
||||
"""Test setting of new operation mode."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("off", state.state)
|
||||
climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("cool", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("cool", state.state)
|
||||
self.assertEqual(('mode-topic', 'cool', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
|
||||
def test_set_operation_pessimistic(self):
|
||||
"""Test setting operation mode in pessimistic mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['mode_state_topic'] = 'mode-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("off", state.state)
|
||||
|
||||
climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("off", state.state)
|
||||
|
||||
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'))
|
||||
self.assertEqual("cool", state.state)
|
||||
|
||||
fire_mqtt_message(self.hass, 'mode-state', 'bogus mode')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("cool", state.attributes.get('operation_mode'))
|
||||
self.assertEqual("cool", state.state)
|
||||
|
||||
def test_set_fan_mode_bad_attr(self):
|
||||
"""Test setting fan mode without required attribute."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("low", state.attributes.get('fan_mode'))
|
||||
climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("low", state.attributes.get('fan_mode'))
|
||||
|
||||
def test_set_fan_mode_pessimistic(self):
|
||||
"""Test setting of new fan mode in pessimistic mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['fan_mode_state_topic'] = 'fan-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("low", state.attributes.get('fan_mode'))
|
||||
|
||||
climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
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'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'fan-state', 'bogus mode')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('high', state.attributes.get('fan_mode'))
|
||||
|
||||
def test_set_fan_mode(self):
|
||||
"""Test setting of new fan mode."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("low", state.attributes.get('fan_mode'))
|
||||
climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('fan-mode-topic', 'high', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('high', state.attributes.get('fan_mode'))
|
||||
|
||||
def test_set_swing_mode_bad_attr(self):
|
||||
"""Test setting swing mode without required attribute."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('swing_mode'))
|
||||
climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('swing_mode'))
|
||||
|
||||
def test_set_swing_pessimistic(self):
|
||||
"""Test setting swing mode in pessimistic mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['swing_mode_state_topic'] = 'swing-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('swing_mode'))
|
||||
|
||||
climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
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'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'swing-state', 'bogus state')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("on", state.attributes.get('swing_mode'))
|
||||
|
||||
def test_set_swing(self):
|
||||
"""Test setting of new swing mode."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("off", state.attributes.get('swing_mode'))
|
||||
climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('swing-mode-topic', 'on', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual("on", state.attributes.get('swing_mode'))
|
||||
|
||||
def test_set_target_temperature(self):
|
||||
"""Test setting the target temperature."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(21, state.attributes.get('temperature'))
|
||||
climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('mode-topic', 'heat', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
climate.set_temperature(self.hass, temperature=47,
|
||||
entity_id=ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(47, state.attributes.get('temperature'))
|
||||
self.assertEqual(('temperature-topic', 47, 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
|
||||
def test_set_target_temperature_pessimistic(self):
|
||||
"""Test setting the target temperature."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['temperature_state_topic'] = 'temperature-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(21, state.attributes.get('temperature'))
|
||||
climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
climate.set_temperature(self.hass, temperature=47,
|
||||
entity_id=ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(21, state.attributes.get('temperature'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-state', '1701')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(1701, state.attributes.get('temperature'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-state', 'not a number')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(1701, state.attributes.get('temperature'))
|
||||
|
||||
def test_receive_mqtt_temperature(self):
|
||||
"""Test getting the current temperature via MQTT."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['current_temperature_topic'] = 'current_temperature'
|
||||
mock_component(self.hass, 'mqtt')
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
fire_mqtt_message(self.hass, 'current_temperature', '47')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(47, state.attributes.get('current_temperature'))
|
||||
|
||||
def test_set_away_mode_pessimistic(self):
|
||||
"""Test setting of the away mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['away_mode_state_topic'] = 'away-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||
|
||||
climate.set_away_mode(self.hass, True, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
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'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'away-state', 'OFF')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'away-state', 'nonsense')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||
|
||||
def test_set_away_mode(self):
|
||||
"""Test setting of the away mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['payload_on'] = 'AN'
|
||||
config['climate']['payload_off'] = 'AUS'
|
||||
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||
climate.set_away_mode(self.hass, True, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('away-mode-topic', 'AN', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('on', state.attributes.get('away_mode'))
|
||||
|
||||
climate.set_away_mode(self.hass, False, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('away-mode-topic', 'AUS', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('away_mode'))
|
||||
|
||||
def test_set_hold_pessimistic(self):
|
||||
"""Test setting the hold mode in pessimistic mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['hold_state_topic'] = 'hold-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(None, state.attributes.get('hold_mode'))
|
||||
|
||||
climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(None, state.attributes.get('hold_mode'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'hold-state', 'on')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('on', state.attributes.get('hold_mode'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'hold-state', 'off')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('hold_mode'))
|
||||
|
||||
def test_set_hold(self):
|
||||
"""Test setting the hold mode."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual(None, state.attributes.get('hold_mode'))
|
||||
climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('hold-topic', 'on', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('on', state.attributes.get('hold_mode'))
|
||||
|
||||
climate.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('hold-topic', 'off', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('hold_mode'))
|
||||
|
||||
def test_set_aux_pessimistic(self):
|
||||
"""Test setting of the aux heating in pessimistic mode."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['aux_state_topic'] = 'aux-state'
|
||||
assert setup_component(self.hass, climate.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||
|
||||
climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'aux-state', 'ON')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('on', state.attributes.get('aux_heat'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'aux-state', 'OFF')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||
|
||||
fire_mqtt_message(self.hass, 'aux-state', 'nonsense')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||
|
||||
def test_set_aux(self):
|
||||
"""Test setting of the aux heating."""
|
||||
assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
||||
climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('aux-topic', 'ON', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('on', state.attributes.get('aux_heat'))
|
||||
|
||||
climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(('aux-topic', 'OFF', 0, False),
|
||||
self.mock_publish.mock_calls[-2][1])
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
self.assertEqual('off', state.attributes.get('aux_heat'))
|
Loading…
Add table
Reference in a new issue