diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 6f9b1720102..acbc2731846 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -132,9 +132,11 @@ ABBREVIATIONS = { "spds": "speeds", "src_type": "source_type", "stat_clsd": "state_closed", + "stat_closing": "state_closing", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_opening": "state_opening", "stat_locked": "state_locked", "stat_unlocked": "state_unlocked", "stat_t": "state_topic", diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index e6cfab90c26..4f2f29f94fb 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -64,7 +66,9 @@ CONF_PAYLOAD_STOP = "payload_stop" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" CONF_STATE_CLOSED = "state_closed" +CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" +CONF_STATE_OPENING = "state_opening" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" @@ -131,7 +135,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, + vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, + vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION @@ -289,12 +295,20 @@ class MqttCover( payload = template.async_render_with_possible_json_value(payload) if payload == self._config[CONF_STATE_OPEN]: - self._state = False + self._state = STATE_OPEN + elif payload == self._config[CONF_STATE_OPENING]: + self._state = STATE_OPENING elif payload == self._config[CONF_STATE_CLOSED]: - self._state = True + self._state = STATE_CLOSED + elif payload == self._config[CONF_STATE_CLOSING]: + self._state = STATE_CLOSING else: - _LOGGER.warning("Payload is not True or False: %s", payload) + _LOGGER.warning( + "Payload is not supported (e.g. open, closed, opening, closing): %s", + payload, + ) return + self.async_write_ha_state() @callback @@ -309,7 +323,11 @@ class MqttCover( float(payload), COVER_PAYLOAD ) self._position = percentage_payload - self._state = percentage_payload == DEFAULT_POSITION_CLOSED + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) else: _LOGGER.warning("Payload is not integer within range: %s", payload) return @@ -370,8 +388,21 @@ class MqttCover( @property def is_closed(self): - """Return if the cover is closed.""" - return self._state + """Return true if the cover is closed or None if the status is unknown.""" + if self._state is None: + return None + + return self._state == STATE_CLOSED + + @property + def is_opening(self): + """Return true if the cover is actively opening.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return true if the cover is actively closing.""" + return self._state == STATE_CLOSING @property def current_cover_position(self): @@ -423,7 +454,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = False + self._state = STATE_OPEN if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_OPEN], COVER_PAYLOAD @@ -444,7 +475,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = True + self._state = STATE_CLOSED if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_CLOSED], COVER_PAYLOAD @@ -538,7 +569,11 @@ class MqttCover( self._config[CONF_RETAIN], ) if self._optimistic: - self._state = percentage_position == self._config[CONF_POSITION_CLOSED] + self._state = ( + STATE_CLOSED + if percentage_position == self._config[CONF_POSITION_CLOSED] + else STATE_OPEN + ) self._position = percentage_position self.async_write_ha_state() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index b15518961a4..128c18de8df 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -19,7 +19,9 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -67,6 +69,93 @@ async def test_state_via_state_topic(hass, mqtt_mock): assert state.state == STATE_OPEN +async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock): + """Test the controlling opening and closing state via a custom payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "state_opening": "34", + "state_closing": "--43", + } + }, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "34") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPENING + + async_fire_mqtt_message(hass, "state-topic", "--43") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSING + + async_fire_mqtt_message(hass, "state-topic", STATE_CLOSED) + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + +async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): + """Test the state after setting the position using optimistic mode.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "position-topic", + "set_position_topic": "set-position-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "optimistic": True, + } + }, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 0}, + blocking=True, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 100}, + blocking=True, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_position_via_position_topic(hass, mqtt_mock): """Test the controlling state via topic.""" assert await async_setup_component(