Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing MQTT lock.open #60022

Merged
merged 11 commits into from Nov 25, 2021
27 changes: 26 additions & 1 deletion homeassistant/components/mqtt/lock.py
Expand Up @@ -4,7 +4,7 @@
import voluptuous as vol

from homeassistant.components import lock
from homeassistant.components.lock import LockEntity
from homeassistant.components.lock import SUPPORT_OPEN, LockEntity
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
Expand All @@ -19,6 +19,7 @@

CONF_PAYLOAD_LOCK = "payload_lock"
CONF_PAYLOAD_UNLOCK = "payload_unlock"
CONF_PAYLOAD_OPEN = "payload_open"

CONF_STATE_LOCKED = "state_locked"
CONF_STATE_UNLOCKED = "state_unlocked"
Expand All @@ -27,6 +28,7 @@
DEFAULT_OPTIMISTIC = False
DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"

Expand All @@ -43,6 +45,7 @@
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string,
vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string,
vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
Expand Down Expand Up @@ -145,6 +148,11 @@ def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic

@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN if CONF_PAYLOAD_OPEN in self._config else 0

async def async_lock(self, **kwargs):
"""Lock the device.

Expand Down Expand Up @@ -178,3 +186,20 @@ async def async_unlock(self, **kwargs):
# Optimistically assume that the lock has changed state.
self._state = False
self.async_write_ha_state()

async def async_open(self, **kwargs):
"""Open the door latch.

This method is a coroutine.
"""
await mqtt.async_publish(
self.hass,
self._config[CONF_COMMAND_TOPIC],
self._config[CONF_PAYLOAD_OPEN],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
if self._optimistic:
# Optimistically assume that the lock unlocks when opened.
self._state = False
self.async_write_ha_state()
125 changes: 124 additions & 1 deletion tests/components/mqtt/test_lock.py
Expand Up @@ -6,12 +6,18 @@
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_UNLOCKED,
SUPPORT_OPEN,
)
from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED
from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
)
from homeassistant.setup import async_setup_component

from .test_common import (
Expand Down Expand Up @@ -69,6 +75,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert not state.attributes.get(ATTR_ASSUMED_STATE)
assert not state.attributes.get(ATTR_SUPPORTED_FEATURES)

async_fire_mqtt_message(hass, "state-topic", "LOCKED")

Expand Down Expand Up @@ -278,6 +285,122 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock):
assert state.attributes.get(ATTR_ASSUMED_STATE)


async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock):
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
"""Test open function of the lock without state topic."""
assert await async_setup_component(
hass,
LOCK_DOMAIN,
{
LOCK_DOMAIN: {
"platform": "mqtt",
"name": "test",
"command_topic": "command-topic",
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"payload_open": "OPEN",
"state_locked": "LOCKED",
"state_unlocked": "UNLOCKED",
}
},
)
await hass.async_block_till_done()

state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)


async def test_sending_mqtt_commands_support_open_and_explicit_optimistic(
hass, mqtt_mock
):
"""Test open function of the lock without state topic."""
assert await async_setup_component(
hass,
LOCK_DOMAIN,
{
LOCK_DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"payload_open": "OPEN",
"state_locked": "LOCKED",
"state_unlocked": "UNLOCKED",
"optimistic": True,
}
},
)
await hass.async_block_till_done()

state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)

await hass.services.async_call(
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)

mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_ASSUMED_STATE)
emontnemery marked this conversation as resolved.
Show resolved Hide resolved


async def test_availability_when_connection_lost(hass, mqtt_mock):
"""Test availability after MQTT disconnection."""
await help_test_availability_when_connection_lost(
Expand Down