diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 8e197cc2e02..d10166a9469 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -21,8 +21,8 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - valid_publish_topic, valid_subscribe_topic) + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,6 +37,8 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template' CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_CLOSE = 'payload_close' CONF_PAYLOAD_STOP = 'payload_stop' +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_STATE_OPEN = 'state_open' CONF_STATE_CLOSED = 'state_closed' CONF_TILT_CLOSED_POSITION = 'tilt_closed_value' @@ -50,6 +52,8 @@ DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' DEFAULT_PAYLOAD_CLOSE = 'CLOSE' DEFAULT_PAYLOAD_STOP = 'STOP' +DEFAULT_PAYLOAD_AVAILABLE = 'online' +DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline' DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -69,11 +73,16 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_PAYLOAD_AVAILABLE, + default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -106,6 +115,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), + config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_TILT_COMMAND_TOPIC), config.get(CONF_TILT_STATUS_TOPIC), config.get(CONF_QOS), @@ -115,6 +125,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_OPEN), config.get(CONF_PAYLOAD_CLOSE), config.get(CONF_PAYLOAD_STOP), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_OPTIMISTIC), value_template, config.get(CONF_TILT_OPEN_POSITION), @@ -131,9 +143,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttCover(CoverDevice): """Representation of a cover that can be controlled using MQTT.""" - def __init__(self, name, state_topic, command_topic, tilt_command_topic, - tilt_status_topic, qos, retain, state_open, state_closed, - payload_open, payload_close, payload_stop, + def __init__(self, name, state_topic, command_topic, availability_topic, + tilt_command_topic, tilt_status_topic, qos, retain, + state_open, state_closed, payload_open, payload_close, + payload_stop, payload_available, payload_not_available, optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, tilt_invert, position_topic, set_position_template): @@ -143,12 +156,16 @@ class MqttCover(CoverDevice): self._name = name self._state_topic = state_topic self._command_topic = command_topic + self._availability_topic = availability_topic + self._available = True if availability_topic is None else False self._tilt_command_topic = tilt_command_topic self._tilt_status_topic = tilt_status_topic self._qos = qos self._payload_open = payload_open self._payload_close = payload_close self._payload_stop = payload_stop + self._payload_available = payload_available + self._payload_not_available = payload_not_available self._state_open = state_open self._state_closed = state_closed self._retain = retain @@ -181,8 +198,8 @@ class MqttCover(CoverDevice): self.async_schedule_update_ha_state() @callback - def message_received(topic, payload, qos): - """Handle new MQTT message.""" + def state_message_received(topic, payload, qos): + """Handle new MQTT state messages.""" if self._template is not None: payload = self._template.async_render_with_possible_json_value( payload) @@ -205,12 +222,28 @@ class MqttCover(CoverDevice): self.async_schedule_update_ha_state() + @callback + def availability_message_received(topic, payload, qos): + """Handle new MQTT availability messages.""" + if payload == self._payload_available: + self._available = True + elif payload == self._payload_not_available: + self._available = False + + self.async_schedule_update_ha_state() + if self._state_topic is None: # Force into optimistic mode. self._optimistic = True else: yield from mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + self.hass, self._state_topic, + state_message_received, self._qos) + + if self._availability_topic is not None: + yield from mqtt.async_subscribe( + self.hass, self._availability_topic, + availability_message_received, self._qos) if self._tilt_status_topic is None: self._tilt_optimistic = True @@ -230,6 +263,11 @@ class MqttCover(CoverDevice): """Return the name of the cover.""" return self._name + @property + def available(self) -> bool: + """Return if cover is available.""" + return self._available + @property def is_closed(self): """Return if the cover is closed.""" diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index 8b6202acdff..0b49e21674e 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -2,7 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN,\ + STATE_UNAVAILABLE import homeassistant.components.cover as cover from homeassistant.components.cover.mqtt import MqttCover @@ -570,71 +571,149 @@ class TestCoverMQTT(unittest.TestCase): def test_find_percentage_in_range_defaults(self): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 100, 0, 0, 100, False, False, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 100, 0, 0, 100, False, False, None, None) self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) def test_find_percentage_in_range_altered(self): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 180, 80, 80, 180, False, False, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 180, 80, 80, 180, False, False, None, None) self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) def test_find_percentage_in_range_defaults_inverted(self): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 100, 0, 0, 100, False, True, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 100, 0, 0, 100, False, True, None, None) self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) def test_find_percentage_in_range_altered_inverted(self): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 180, 80, 80, 180, False, True, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 180, 80, 80, 180, False, True, None, None) self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) def test_find_in_range_defaults(self): """Test find in range with default range.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 100, 0, 0, 100, False, False, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 100, 0, 0, 100, False, False, None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) def test_find_in_range_altered(self): """Test find in range with altered range.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 180, 80, 80, 180, False, False, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 180, 80, 80, 180, False, False, None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) def test_find_in_range_defaults_inverted(self): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 100, 0, 0, 100, False, True, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 100, 0, 0, 100, False, True, None, None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) def test_find_in_range_altered_inverted(self): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( - 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, - 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, - 180, 80, 80, 180, False, True, None, None) + 'cover.test', 'state-topic', 'command-topic', None, + 'tilt-command-topic', 'tilt-status-topic', 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, + False, None, 180, 80, 80, 180, False, True, None, None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) + + def test_availability_without_topic(self): + """Test availability without defined availability topic.""" + self.assertTrue(setup_component(self.hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic' + } + })) + + state = self.hass.states.get('cover.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + def test_availability_by_defaults(self): + """Test availability by defaults with defined topic.""" + self.assertTrue(setup_component(self.hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability-topic' + } + })) + + state = self.hass.states.get('cover.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('cover.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('cover.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + def test_availability_by_custom_payload(self): + """Test availability by custom payload with defined topic.""" + self.assertTrue(setup_component(self.hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + })) + + state = self.hass.states.get('cover.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'good') + self.hass.block_till_done() + + state = self.hass.states.get('cover.test') + self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability-topic', 'nogood') + self.hass.block_till_done() + + state = self.hass.states.get('cover.test') + self.assertEqual(STATE_UNAVAILABLE, state.state)