Add jammed state support for MQTT lock (#86010)

* Add jammed state support for MQTT lock

* Correct payload jammed key

* Add tests - rename solved to ok

* Rename jammed state and template topics to motor

* Use state topic for handling motor state

* Follow up comments

* Change default behaviour `state_unjammed`

* Skip `state_unjammed`
This commit is contained in:
Jan Bouwhuis 2023-01-20 12:19:26 +01:00 committed by GitHub
parent c8b9260f92
commit 92742ae423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 13 deletions

View File

@ -198,6 +198,7 @@ ABBREVIATIONS = {
"stat_cla": "state_class",
"stat_clsd": "state_closed",
"stat_closing": "state_closing",
"stat_jam": "state_jammed",
"stat_off": "state_off",
"stat_on": "state_on",
"stat_open": "state_open",

View File

@ -43,6 +43,7 @@ CONF_STATE_LOCKED = "state_locked"
CONF_STATE_LOCKING = "state_locking"
CONF_STATE_UNLOCKED = "state_unlocked"
CONF_STATE_UNLOCKING = "state_unlocking"
CONF_STATE_JAMMED = "state_jammed"
DEFAULT_NAME = "MQTT Lock"
DEFAULT_PAYLOAD_LOCK = "LOCK"
@ -52,6 +53,7 @@ DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_LOCKING = "LOCKING"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
DEFAULT_STATE_JAMMED = "JAMMED"
MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
{
@ -66,6 +68,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
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_JAMMED, default=DEFAULT_STATE_JAMMED): 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,
@ -83,6 +86,7 @@ PLATFORM_SCHEMA = vol.All(
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
STATE_CONFIG_KEYS = [
CONF_STATE_JAMMED,
CONF_STATE_LOCKED,
CONF_STATE_LOCKING,
CONF_STATE_UNLOCKED,
@ -157,15 +161,20 @@ class MqttLock(MqttEntity, LockEntity):
def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
topics: dict[str, dict[str, Any]] = {}
qos: int = self._config[CONF_QOS]
encoding: str | None = self._config[CONF_ENCODING] or None
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
"""Handle new lock state messages."""
payload = self._value_template(msg.payload)
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]
self._attr_is_jammed = payload == self._config[CONF_STATE_JAMMED]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
@ -173,18 +182,18 @@ class MqttLock(MqttEntity, LockEntity):
# Force into optimistic mode.
self._optimistic = True
else:
self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass,
self._sub_state,
{
"state_topic": {
"topic": self._config.get(CONF_STATE_TOPIC),
"msg_callback": message_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
},
)
topics[CONF_STATE_TOPIC] = {
"topic": self._config.get(CONF_STATE_TOPIC),
"msg_callback": message_received,
CONF_QOS: qos,
CONF_ENCODING: encoding,
}
self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass,
self._sub_state,
topics,
)
async def _subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""

View File

@ -8,6 +8,7 @@ from homeassistant.components.lock import (
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_UNLOCKED,
@ -21,6 +22,7 @@ from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .test_common import (
@ -468,6 +470,112 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic(
assert state.attributes.get(ATTR_ASSUMED_STATE)
async def test_sending_mqtt_commands_pessimistic(
hass: HomeAssistant, mqtt_mock_entry_with_yaml_config
) -> None:
"""Test function of the lock with state topics."""
assert await async_setup_component(
hass,
mqtt.DOMAIN,
{
mqtt.DOMAIN: {
lock.DOMAIN: {
"name": "test",
"command_topic": "command-topic",
"state_topic": "state-topic",
"payload_lock": "LOCK",
"payload_unlock": "UNLOCK",
"payload_open": "OPEN",
"state_locked": "LOCKED",
"state_locking": "LOCKING",
"state_unlocked": "UNLOCKED",
"state_unlocking": "UNLOCKING",
"state_jammed": "JAMMED",
}
}
},
)
await hass.async_block_till_done()
mqtt_mock = await mqtt_mock_entry_with_yaml_config()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN
# send lock command to lock
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()
# receive state from lock
async_fire_mqtt_message(hass, "state-topic", "LOCKED")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
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()
# receive state from lock
async_fire_mqtt_message(hass, "state-topic", "UNLOCKED")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
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()
# receive state from lock
async_fire_mqtt_message(hass, "state-topic", "UNLOCKED")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_UNLOCKED
# send lock command to lock
await hass.services.async_call(
lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True
)
# Go to locking state
mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False)
mqtt_mock.async_publish.reset_mock()
# receive locking state from lock
async_fire_mqtt_message(hass, "state-topic", "LOCKING")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKING
# receive jammed state from lock
async_fire_mqtt_message(hass, "state-topic", "JAMMED")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_JAMMED
# receive solved state from lock
async_fire_mqtt_message(hass, "state-topic", "LOCKED")
await hass.async_block_till_done()
state = hass.states.get("lock.test")
assert state.state is STATE_LOCKED
async def test_availability_when_connection_lost(
hass, mqtt_mock_entry_with_yaml_config
):