diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index b107710fea5..7436ffc41ea 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -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): diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 61b481ed4db..894fc290c38 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -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