mirror of https://github.com/home-assistant/core
Add MQTT climate two-point target temperature support (#22860)
* Add MQTT climate two-point target temperature support * Sort * Fix test
This commit is contained in:
parent
36c135c785
commit
c9ec166f4b
|
@ -10,7 +10,10 @@ from homeassistant.components.climate.const import (
|
|||
ATTR_OPERATION_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, STATE_AUTO,
|
||||
STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH)
|
||||
from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_OFF,
|
||||
|
@ -41,6 +44,10 @@ 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_TEMPERATURE_LOW_COMMAND_TOPIC = 'temperature_low_command_topic'
|
||||
CONF_TEMPERATURE_LOW_STATE_TOPIC = 'temperature_low_state_topic'
|
||||
CONF_TEMPERATURE_HIGH_COMMAND_TOPIC = 'temperature_high_command_topic'
|
||||
CONF_TEMPERATURE_HIGH_STATE_TOPIC = 'temperature_high_state_topic'
|
||||
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'
|
||||
|
@ -130,6 +137,12 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
|||
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_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_HIGH_COMMAND_TOPIC):
|
||||
mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_HIGH_STATE_TOPIC):
|
||||
mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
|
@ -186,6 +199,8 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
self._topic = None
|
||||
self._value_templates = None
|
||||
self._target_temperature = None
|
||||
self._target_temperature_low = None
|
||||
self._target_temperature_high = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._current_swing_mode = None
|
||||
|
@ -230,6 +245,8 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_LOW_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_HIGH_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_SWING_MODE_COMMAND_TOPIC,
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC,
|
||||
|
@ -238,6 +255,8 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
CONF_POWER_STATE_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_LOW_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_HIGH_STATE_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
CONF_AWAY_MODE_STATE_TOPIC,
|
||||
|
@ -250,8 +269,16 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
# set to None in non-optimistic mode
|
||||
self._target_temperature = self._current_fan_mode = \
|
||||
self._current_operation = self._current_swing_mode = None
|
||||
self._target_temperature_low = None
|
||||
self._target_temperature_high = None
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
self._target_temperature = config[CONF_TEMP_INITIAL]
|
||||
if self._topic[CONF_TEMPERATURE_LOW_STATE_TOPIC] is None:
|
||||
self._target_temperature_low = config[CONF_TEMP_INITIAL]
|
||||
if self._topic[CONF_TEMPERATURE_HIGH_STATE_TOPIC] is None:
|
||||
self._target_temperature_high = config[CONF_TEMP_INITIAL]
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = SPEED_LOW
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
|
@ -339,6 +366,38 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
'msg_callback': handle_temperature_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_temperature_low_received(msg):
|
||||
"""Handle target temperature low coming via MQTT."""
|
||||
try:
|
||||
self._target_temperature_low = float(msg.payload)
|
||||
self.async_write_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse low temperature from %s",
|
||||
msg.payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_LOW_STATE_TOPIC] is not None:
|
||||
topics[CONF_TEMPERATURE_LOW_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_TEMPERATURE_LOW_STATE_TOPIC],
|
||||
'msg_callback': handle_temperature_low_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_temperature_high_received(msg):
|
||||
"""Handle target temperature high coming via MQTT."""
|
||||
try:
|
||||
self._target_temperature_high = float(msg.payload)
|
||||
self.async_write_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse high temperature from %s",
|
||||
msg.payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_HIGH_STATE_TOPIC] is not None:
|
||||
topics[CONF_TEMPERATURE_HIGH_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_TEMPERATURE_HIGH_STATE_TOPIC],
|
||||
'msg_callback': handle_temperature_high_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_fan_mode_received(msg):
|
||||
"""Handle receiving fan mode via MQTT."""
|
||||
|
@ -498,6 +557,16 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the low target temperature we try to reach."""
|
||||
return self._target_temperature_low
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the high target temperature we try to reach."""
|
||||
return self._target_temperature_high
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
|
@ -556,6 +625,31 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
kwargs.get(ATTR_TEMPERATURE), self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN])
|
||||
|
||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None:
|
||||
if self._topic[CONF_TEMPERATURE_LOW_STATE_TOPIC] is None:
|
||||
# optimistic mode
|
||||
self._target_temperature_low = kwargs[ATTR_TARGET_TEMP_LOW]
|
||||
|
||||
if (self._config[CONF_SEND_IF_OFF] or
|
||||
self._current_operation != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_LOW_COMMAND_TOPIC],
|
||||
kwargs.get(ATTR_TARGET_TEMP_LOW), self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN])
|
||||
|
||||
if kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
||||
if self._topic[CONF_TEMPERATURE_HIGH_STATE_TOPIC] is None:
|
||||
# optimistic mode
|
||||
self._target_temperature_high = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||
|
||||
if (self._config[CONF_SEND_IF_OFF] or
|
||||
self._current_operation != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass,
|
||||
self._topic[CONF_TEMPERATURE_HIGH_COMMAND_TOPIC],
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH), self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN])
|
||||
|
||||
# Always optimistic?
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -691,6 +785,14 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
|
|||
(self._topic[CONF_TEMPERATURE_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
if (self._topic[CONF_TEMPERATURE_LOW_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_TEMPERATURE_LOW_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_TARGET_TEMPERATURE_LOW
|
||||
|
||||
if (self._topic[CONF_TEMPERATURE_HIGH_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_TEMPERATURE_HIGH_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
|
||||
if (self._topic[CONF_MODE_COMMAND_TOPIC] is not None) or \
|
||||
(self._topic[CONF_MODE_STATE_TOPIC] is not None):
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
|
|
|
@ -15,7 +15,8 @@ from homeassistant.components.climate.const import (
|
|||
SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, STATE_AUTO,
|
||||
STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY)
|
||||
STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_TARGET_TEMPERATURE_HIGH)
|
||||
from homeassistant.components.mqtt.discovery import async_start
|
||||
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
|
||||
from homeassistant.setup import setup_component
|
||||
|
@ -35,6 +36,8 @@ DEFAULT_CONFIG = {
|
|||
'name': 'test',
|
||||
'mode_command_topic': 'mode-topic',
|
||||
'temperature_command_topic': 'temperature-topic',
|
||||
'temperature_low_command_topic': 'temperature-low-topic',
|
||||
'temperature_high_command_topic': 'temperature-high-topic',
|
||||
'fan_mode_command_topic': 'fan-mode-topic',
|
||||
'swing_mode_command_topic': 'swing-mode-topic',
|
||||
'away_mode_command_topic': 'away-mode-topic',
|
||||
|
@ -75,7 +78,9 @@ class TestMQTTClimate(unittest.TestCase):
|
|||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
support = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_SWING_MODE | SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE | SUPPORT_AUX_HEAT)
|
||||
SUPPORT_HOLD_MODE | SUPPORT_AUX_HEAT |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW |
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH)
|
||||
|
||||
assert state.attributes.get("supported_features") == support
|
||||
|
||||
|
@ -341,6 +346,66 @@ class TestMQTTClimate(unittest.TestCase):
|
|||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert 1701 == state.attributes.get('temperature')
|
||||
|
||||
def test_set_target_temperature_low_high(self):
|
||||
"""Test setting the low/high target temperature."""
|
||||
assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
|
||||
|
||||
common.set_temperature(self.hass, target_temp_low=20,
|
||||
target_temp_high=23,
|
||||
entity_id=ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
print(state.attributes)
|
||||
assert 20 == state.attributes.get('target_temp_low')
|
||||
assert 23 == state.attributes.get('target_temp_high')
|
||||
self.mock_publish.async_publish.assert_any_call(
|
||||
'temperature-low-topic', 20, 0, False)
|
||||
self.mock_publish.async_publish.assert_any_call(
|
||||
'temperature-high-topic', 23, 0, False)
|
||||
|
||||
def test_set_target_temperature_low_highpessimistic(self):
|
||||
"""Test setting the low/high target temperature."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config['climate']['temperature_low_state_topic'] = \
|
||||
'temperature-low-state'
|
||||
config['climate']['temperature_high_state_topic'] = \
|
||||
'temperature-high-state'
|
||||
assert setup_component(self.hass, CLIMATE_DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get('target_temp_low') is None
|
||||
assert state.attributes.get('target_temp_high') is None
|
||||
self.hass.block_till_done()
|
||||
common.set_temperature(self.hass, target_temp_low=20,
|
||||
target_temp_high=23,
|
||||
entity_id=ENTITY_CLIMATE)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get('target_temp_low') is None
|
||||
assert state.attributes.get('target_temp_high') is None
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-low-state', '1701')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert 1701 == state.attributes.get('target_temp_low')
|
||||
assert state.attributes.get('target_temp_high') is None
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-high-state', '1703')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert 1701 == state.attributes.get('target_temp_low')
|
||||
assert 1703 == state.attributes.get('target_temp_high')
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-low-state', 'not a number')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert 1701 == state.attributes.get('target_temp_low')
|
||||
|
||||
fire_mqtt_message(self.hass, 'temperature-high-state', 'not a number')
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||
assert 1703 == state.attributes.get('target_temp_high')
|
||||
|
||||
def test_receive_mqtt_temperature(self):
|
||||
"""Test getting the current temperature via MQTT."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
|
|
Loading…
Reference in New Issue