Reconfigure MQTT climate component if discovery info is changed (#18174)
parent
986ca23934
commit
e41af133fc
|
@ -22,7 +22,8 @@ from homeassistant.const import (
|
|||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate)
|
||||
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate,
|
||||
subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -77,6 +78,18 @@ CONF_MIN_TEMP = 'min_temp'
|
|||
CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TEMP_STEP = 'temp_step'
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
|
@ -153,69 +166,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=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_entities([
|
||||
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
|
||||
)
|
||||
},
|
||||
value_templates,
|
||||
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),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_MIN_TEMP),
|
||||
config.get(CONF_MAX_TEMP),
|
||||
config.get(CONF_TEMP_STEP),
|
||||
config,
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
|
@ -223,54 +177,130 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
|||
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
||||
"""Representation of an MQTT climate device."""
|
||||
|
||||
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,
|
||||
min_temp, max_temp, temp_step, discovery_hash):
|
||||
def __init__(self, hass, config, discovery_hash):
|
||||
"""Initialize the climate device."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
self._config = config
|
||||
self._sub_state = None
|
||||
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._value_templates = value_templates
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
# set to None in non-optimistic mode
|
||||
self._target_temperature = self._current_fan_mode = \
|
||||
self._current_operation = self._current_swing_mode = None
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
self._target_temperature = target_temperature
|
||||
self._name = None
|
||||
self._topic = None
|
||||
self._value_templates = None
|
||||
self._qos = None
|
||||
self._retain = None
|
||||
self._target_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._current_swing_mode = None
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._away = False
|
||||
self._hold = None
|
||||
self._current_temperature = None
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = current_fan_mode
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
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 = temp_step
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._discovery_hash = discovery_hash
|
||||
self._aux = False
|
||||
self._fan_list = None
|
||||
self._operation_list = None
|
||||
self._swing_list = None
|
||||
self._target_temperature_step = None
|
||||
self._send_if_off = None
|
||||
self._payload_on = None
|
||||
self._payload_off = None
|
||||
self._min_temp = None
|
||||
self._max_temp = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, self._qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._setup_from_config(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._topic = {
|
||||
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
|
||||
)
|
||||
}
|
||||
self._qos = config.get(CONF_QOS)
|
||||
self._retain = config.get(CONF_RETAIN)
|
||||
self._operation_list = config.get(CONF_MODE_LIST)
|
||||
self._fan_list = config.get(CONF_FAN_MODE_LIST)
|
||||
self._swing_list = config.get(CONF_SWING_MODE_LIST)
|
||||
|
||||
# set to None in non-optimistic mode
|
||||
self._target_temperature = self._current_fan_mode = \
|
||||
self._current_operation = self._current_swing_mode = None
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
self._target_temperature = config.get(CONF_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:
|
||||
self._current_swing_mode = STATE_OFF
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = STATE_OFF
|
||||
self._away = False
|
||||
self._hold = None
|
||||
self._aux = False
|
||||
self._send_if_off = config.get(CONF_SEND_IF_OFF)
|
||||
self._payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
self._payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
self._min_temp = config.get(CONF_MIN_TEMP)
|
||||
self._max_temp = config.get(CONF_MAX_TEMP)
|
||||
self._target_temperature_step = config.get(CONF_TEMP_STEP)
|
||||
|
||||
config.get(CONF_AVAILABILITY_TOPIC)
|
||||
config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
|
||||
value_templates = {}
|
||||
if CONF_VALUE_TEMPLATE in config:
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = self.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 = self.hass
|
||||
self._value_templates = value_templates
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
|
||||
@callback
|
||||
def handle_current_temp_received(topic, payload, qos):
|
||||
|
@ -287,9 +317,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
handle_current_temp_received, self._qos)
|
||||
topics[CONF_CURRENT_TEMPERATURE_TOPIC] = {
|
||||
'topic': self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
'msg_callback': handle_current_temp_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_mode_received(topic, payload, qos):
|
||||
|
@ -305,9 +336,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_MODE_STATE_TOPIC],
|
||||
handle_mode_received, self._qos)
|
||||
topics[CONF_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_temperature_received(topic, payload, qos):
|
||||
|
@ -324,9 +356,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
handle_temperature_received, self._qos)
|
||||
topics[CONF_TEMPERATURE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
'msg_callback': handle_temperature_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_fan_mode_received(topic, payload, qos):
|
||||
|
@ -343,9 +376,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
handle_fan_mode_received, self._qos)
|
||||
topics[CONF_FAN_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_fan_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_swing_mode_received(topic, payload, qos):
|
||||
|
@ -362,9 +396,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
handle_swing_mode_received, self._qos)
|
||||
topics[CONF_SWING_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_swing_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_away_mode_received(topic, payload, qos):
|
||||
|
@ -388,9 +423,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
handle_away_mode_received, self._qos)
|
||||
topics[CONF_AWAY_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_away_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_aux_mode_received(topic, payload, qos):
|
||||
|
@ -413,9 +449,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AUX_STATE_TOPIC],
|
||||
handle_aux_mode_received, self._qos)
|
||||
topics[CONF_AUX_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_AUX_STATE_TOPIC],
|
||||
'msg_callback': handle_aux_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
@callback
|
||||
def handle_hold_mode_received(topic, payload, qos):
|
||||
|
@ -428,9 +465,19 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
handle_hold_mode_received, self._qos)
|
||||
topics[CONF_HOLD_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
'msg_callback': handle_hold_mode_received,
|
||||
'qos': self._qos}
|
||||
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
topics)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
|
@ -684,3 +684,34 @@ async def test_discovery_removal_climate(hass, mqtt_mock, caplog):
|
|||
await hass.async_block_till_done()
|
||||
state = hass.states.get('climate.beer')
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_discovery_update_climate(hass, mqtt_mock, caplog):
|
||||
"""Test removal of discovered climate."""
|
||||
entry = MockConfigEntry(domain=mqtt.DOMAIN)
|
||||
await async_start(hass, 'homeassistant', {}, entry)
|
||||
data1 = (
|
||||
'{ "name": "Beer" }'
|
||||
)
|
||||
data2 = (
|
||||
'{ "name": "Milk" }'
|
||||
)
|
||||
async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config',
|
||||
data1)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get('climate.beer')
|
||||
assert state is not None
|
||||
assert state.name == 'Beer'
|
||||
|
||||
async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config',
|
||||
data2)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get('climate.beer')
|
||||
assert state is not None
|
||||
assert state.name == 'Milk'
|
||||
|
||||
state = hass.states.get('climate.milk')
|
||||
assert state is None
|
||||
|
|
Loading…
Reference in New Issue