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_opening": "state_opening",
"stat_stopped": "state_stopped", "stat_stopped": "state_stopped",
"stat_locked": "state_locked", "stat_locked": "state_locked",
"stat_locking": "state_locking",
"stat_unlocked": "state_unlocked", "stat_unlocked": "state_unlocked",
"stat_unlocking": "state_unlocking",
"stat_t": "state_topic", "stat_t": "state_topic",
"stat_tpl": "state_template", "stat_tpl": "state_template",
"stat_val_tpl": "state_value_template", "stat_val_tpl": "state_value_template",

View File

@ -40,14 +40,18 @@ CONF_PAYLOAD_UNLOCK = "payload_unlock"
CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_OPEN = "payload_open"
CONF_STATE_LOCKED = "state_locked" CONF_STATE_LOCKED = "state_locked"
CONF_STATE_LOCKING = "state_locking"
CONF_STATE_UNLOCKED = "state_unlocked" CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking"
DEFAULT_NAME = "MQTT Lock" DEFAULT_NAME = "MQTT Lock"
DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_STATE_LOCKED = "LOCKED" DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_LOCKING = "LOCKING"
DEFAULT_STATE_UNLOCKED = "UNLOCKED" DEFAULT_STATE_UNLOCKED = "UNLOCKED"
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( 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_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): 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_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, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
} }
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@ -76,6 +82,13 @@ PLATFORM_SCHEMA = vol.All(
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -107,6 +120,7 @@ class MqttLock(MqttEntity, LockEntity):
_attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED
_optimistic: bool _optimistic: bool
_valid_states: list[str]
_value_template: Callable[[ReceivePayloadType], ReceivePayloadType] _value_template: Callable[[ReceivePayloadType], ReceivePayloadType]
def __init__( def __init__(
@ -138,6 +152,8 @@ class MqttLock(MqttEntity, LockEntity):
if CONF_PAYLOAD_OPEN in config: if CONF_PAYLOAD_OPEN in config:
self._attr_supported_features |= LockEntityFeature.OPEN self._attr_supported_features |= LockEntityFeature.OPEN
self._valid_states = [config[state] for state in STATE_CONFIG_KEYS]
def _prepare_subscribe_topics(self) -> None: def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
@ -146,10 +162,10 @@ class MqttLock(MqttEntity, LockEntity):
def message_received(msg: ReceiveMessage) -> None: def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
payload = self._value_template(msg.payload) payload = self._value_template(msg.payload)
if payload == self._config[CONF_STATE_LOCKED]: if payload in self._valid_states:
self._attr_is_locked = True self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
elif payload == self._config[CONF_STATE_UNLOCKED]: self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
self._attr_is_locked = False self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self) 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_OPEN,
SERVICE_UNLOCK, SERVICE_UNLOCK,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING,
LockEntityFeature, LockEntityFeature,
) )
from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED
@ -65,7 +67,18 @@ def lock_platform_only():
yield 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.""" """Test the controlling state via topic."""
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -79,7 +92,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi
"payload_lock": "LOCK", "payload_lock": "LOCK",
"payload_unlock": "UNLOCK", "payload_unlock": "UNLOCK",
"state_locked": "LOCKED", "state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED", "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_ASSUMED_STATE)
assert not state.attributes.get(ATTR_SUPPORTED_FEATURES) 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") state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED assert state.state is lock_state
async_fire_mqtt_message(hass, "state-topic", "UNLOCKED")
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
@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( 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.""" """Test the controlling state via topic."""
assert await async_setup_component( assert await async_setup_component(
@ -119,7 +138,9 @@ async def test_controlling_non_default_state_via_topic(
"payload_lock": "LOCK", "payload_lock": "LOCK",
"payload_unlock": "UNLOCK", "payload_unlock": "UNLOCK",
"state_locked": "closed", "state_locked": "closed",
"state_locking": "closing",
"state_unlocked": "open", "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 state.state is STATE_UNLOCKED
assert not state.attributes.get(ATTR_ASSUMED_STATE) 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") state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED assert state.state is lock_state
async_fire_mqtt_message(hass, "state-topic", "open")
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
@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( 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.""" """Test the controlling state via topic and JSON message."""
assert await async_setup_component( assert await async_setup_component(
@ -158,7 +183,9 @@ async def test_controlling_state_via_topic_and_json_message(
"payload_lock": "LOCK", "payload_lock": "LOCK",
"payload_unlock": "UNLOCK", "payload_unlock": "UNLOCK",
"state_locked": "LOCKED", "state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED", "state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
"value_template": "{{ value_json.val }}", "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") state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED 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") state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED assert state.state is lock_state
async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}')
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
@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( 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.""" """Test the controlling state via topic and JSON message."""
assert await async_setup_component( 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_lock": "LOCK",
"payload_unlock": "UNLOCK", "payload_unlock": "UNLOCK",
"state_locked": "closed", "state_locked": "closed",
"state_locking": "closing",
"state_unlocked": "open", "state_unlocked": "open",
"state_unlocking": "opening",
"value_template": "{{ value_json.val }}", "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") state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED 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") state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED assert state.state is lock_state
async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}')
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
async def test_sending_mqtt_commands_and_optimistic( async def test_sending_mqtt_commands_and_optimistic(