Add locking and unlocking to MQTT lock (#85779)

* Implement locking, unlocking and jammed on MQTT lock

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Add tests

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Refactor condition

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Parametrize tests

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Manage only locking and unlocking

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Remove jammed from abbreviations

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* set valid states in self._valid_states

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
Patrick ZAJDA 2023-01-16 08:42:43 +01:00 committed by GitHub
parent d3c41bc31c
commit 0b02abf708
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 82 additions and 36 deletions

View File

@ -204,7 +204,9 @@ ABBREVIATIONS = {
"stat_opening": "state_opening",
"stat_stopped": "state_stopped",
"stat_locked": "state_locked",
"stat_locking": "state_locking",
"stat_unlocked": "state_unlocked",
"stat_unlocking": "state_unlocking",
"stat_t": "state_topic",
"stat_tpl": "state_template",
"stat_val_tpl": "state_value_template",

View File

@ -40,14 +40,18 @@ CONF_PAYLOAD_UNLOCK = "payload_unlock"
CONF_PAYLOAD_OPEN = "payload_open"
CONF_STATE_LOCKED = "state_locked"
CONF_STATE_LOCKING = "state_locking"
CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking"
DEFAULT_NAME = "MQTT Lock"
DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_LOCKING = "LOCKING"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
{
@ -63,7 +67,9 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
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_LOCKING, default=DEFAULT_STATE_LOCKING): cv.string,
vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string,
vol.Optional(CONF_STATE_UNLOCKING, default=DEFAULT_STATE_UNLOCKING): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@ -76,6 +82,13 @@ PLATFORM_SCHEMA = vol.All(
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
STATE_CONFIG_KEYS = [
CONF_STATE_LOCKED,
CONF_STATE_LOCKING,
CONF_STATE_UNLOCKED,
CONF_STATE_UNLOCKING,
]
async def async_setup_entry(
hass: HomeAssistant,
@ -107,6 +120,7 @@ class MqttLock(MqttEntity, LockEntity):
_attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED
_optimistic: bool
_valid_states: list[str]
_value_template: Callable[[ReceivePayloadType], ReceivePayloadType]
def __init__(
@ -138,6 +152,8 @@ class MqttLock(MqttEntity, LockEntity):
if CONF_PAYLOAD_OPEN in config:
self._attr_supported_features |= LockEntityFeature.OPEN
self._valid_states = [config[state] for state in STATE_CONFIG_KEYS]
def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
@ -146,10 +162,10 @@ class MqttLock(MqttEntity, LockEntity):
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
payload = self._value_template(msg.payload)
if payload == self._config[CONF_STATE_LOCKED]:
self._attr_is_locked = True
elif payload == self._config[CONF_STATE_UNLOCKED]:
self._attr_is_locked = False
if payload in self._valid_states:
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)

View File

@ -9,7 +9,9 @@ from homeassistant.components.lock import (
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_LOCKING,
STATE_UNLOCKED,
STATE_UNLOCKING,
LockEntityFeature,
)
from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED
@ -65,7 +67,18 @@ def lock_platform_only():
yield
async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config):
@pytest.mark.parametrize(
"payload,lock_state",
[
("LOCKED", STATE_LOCKED),
("LOCKING", STATE_LOCKING),
("UNLOCKED", STATE_UNLOCKED),
("UNLOCKING", STATE_UNLOCKING),
],
)
async def test_controlling_state_via_topic(
hass, mqtt_mock_entry_with_yaml_config, payload, lock_state
):
"""Test the controlling state via topic."""
assert await async_setup_component(
hass,
@ -79,7 +92,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
}
}
},
@ -92,19 +107,23 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi
assert not state.attributes.get(ATTR_ASSUMED_STATE)
assert not state.attributes.get(ATTR_SUPPORTED_FEATURES)
async_fire_mqtt_message(hass, "state-topic", "LOCKED")
async_fire_mqtt_message(hass, "state-topic", payload)
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
async_fire_mqtt_message(hass, "state-topic", "UNLOCKED")
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.state is lock_state
@pytest.mark.parametrize(
"payload,lock_state",
[
("closed", STATE_LOCKED),
("closing", STATE_LOCKING),
("open", STATE_UNLOCKED),
("opening", STATE_UNLOCKING),
],
)
async def test_controlling_non_default_state_via_topic(
hass, mqtt_mock_entry_with_yaml_config
hass, mqtt_mock_entry_with_yaml_config, payload, lock_state
):
"""Test the controlling state via topic."""
assert await async_setup_component(
@ -119,7 +138,9 @@ async def test_controlling_non_default_state_via_topic(
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"state_locked": "closed",
"state_locking": "closing",
"state_unlocked": "open",
"state_unlocking": "opening",
}
}
},
@ -131,19 +152,23 @@ async def test_controlling_non_default_state_via_topic(
assert state.state is STATE_UNLOCKED
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "state-topic", "closed")
async_fire_mqtt_message(hass, "state-topic", payload)
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
async_fire_mqtt_message(hass, "state-topic", "open")
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.state is lock_state
@pytest.mark.parametrize(
"payload,lock_state",
[
('{"val":"LOCKED"}', STATE_LOCKED),
('{"val":"LOCKING"}', STATE_LOCKING),
('{"val":"UNLOCKED"}', STATE_UNLOCKED),
('{"val":"UNLOCKING"}', STATE_UNLOCKING),
],
)
async def test_controlling_state_via_topic_and_json_message(
hass, mqtt_mock_entry_with_yaml_config
hass, mqtt_mock_entry_with_yaml_config, payload, lock_state
):
"""Test the controlling state via topic and JSON message."""
assert await async_setup_component(
@ -158,7 +183,9 @@ async def test_controlling_state_via_topic_and_json_message(
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
"value_template": "{{ value_json.val }}",
}
}
@ -170,19 +197,23 @@ async def test_controlling_state_via_topic_and_json_message(
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCKED"}')
async_fire_mqtt_message(hass, "state-topic", payload)
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}')
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.state is lock_state
@pytest.mark.parametrize(
"payload,lock_state",
[
('{"val":"closed"}', STATE_LOCKED),
('{"val":"closing"}', STATE_LOCKING),
('{"val":"open"}', STATE_UNLOCKED),
('{"val":"opening"}', STATE_UNLOCKING),
],
)
async def test_controlling_non_default_state_via_topic_and_json_message(
hass, mqtt_mock_entry_with_yaml_config
hass, mqtt_mock_entry_with_yaml_config, payload, lock_state
):
"""Test the controlling state via topic and JSON message."""
assert await async_setup_component(
@ -197,7 +228,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message(
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"state_locked": "closed",
"state_locking": "closing",
"state_unlocked": "open",
"state_unlocking": "opening",
"value_template": "{{ value_json.val }}",
}
}
@ -209,15 +242,10 @@ async def test_controlling_non_default_state_via_topic_and_json_message(
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
async_fire_mqtt_message(hass, "state-topic", '{"val":"closed"}')
async_fire_mqtt_message(hass, "state-topic", payload)
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}')
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.state is lock_state
async def test_sending_mqtt_commands_and_optimistic(