mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
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:
parent
d3c41bc31c
commit
0b02abf708
@ -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",
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user