Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/mqtt/abbreviations.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,11 @@
"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",
Expand Down
53 changes: 44 additions & 9 deletions homeassistant/components/mqtt/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
CONF_OPTIMISTIC,
CONF_VALUE_TEMPLATE,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
STATE_UNKNOWN,
)
from homeassistant.core import callback
Expand Down Expand Up @@ -64,7 +66,9 @@
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"
Expand Down Expand Up @@ -131,7 +135,9 @@ def validate_options(value):
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
Expand Down Expand Up @@ -289,12 +295,20 @@ def state_message_received(msg):
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
Expand All @@ -309,7 +323,11 @@ def position_message_received(msg):
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
Expand Down Expand Up @@ -370,8 +388,21 @@ def name(self):

@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):
Expand Down Expand Up @@ -423,7 +454,7 @@ async def async_open_cover(self, **kwargs):
)
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
Expand All @@ -444,7 +475,7 @@ async def async_close_cover(self, **kwargs):
)
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
Expand Down Expand Up @@ -538,7 +569,11 @@ async def async_set_cover_position(self, **kwargs):
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()

Expand Down
89 changes: 89 additions & 0 deletions tests/components/mqtt/test_cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
SERVICE_TOGGLE,
SERVICE_TOGGLE_COVER_TILT,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
Expand Down Expand Up @@ -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(
Expand Down