mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
c8b9260f92
commit
92742ae423
@ -198,6 +198,7 @@ ABBREVIATIONS = {
|
|||||||
"stat_cla": "state_class",
|
"stat_cla": "state_class",
|
||||||
"stat_clsd": "state_closed",
|
"stat_clsd": "state_closed",
|
||||||
"stat_closing": "state_closing",
|
"stat_closing": "state_closing",
|
||||||
|
"stat_jam": "state_jammed",
|
||||||
"stat_off": "state_off",
|
"stat_off": "state_off",
|
||||||
"stat_on": "state_on",
|
"stat_on": "state_on",
|
||||||
"stat_open": "state_open",
|
"stat_open": "state_open",
|
||||||
|
@ -43,6 +43,7 @@ CONF_STATE_LOCKED = "state_locked"
|
|||||||
CONF_STATE_LOCKING = "state_locking"
|
CONF_STATE_LOCKING = "state_locking"
|
||||||
CONF_STATE_UNLOCKED = "state_unlocked"
|
CONF_STATE_UNLOCKED = "state_unlocked"
|
||||||
CONF_STATE_UNLOCKING = "state_unlocking"
|
CONF_STATE_UNLOCKING = "state_unlocking"
|
||||||
|
CONF_STATE_JAMMED = "state_jammed"
|
||||||
|
|
||||||
DEFAULT_NAME = "MQTT Lock"
|
DEFAULT_NAME = "MQTT Lock"
|
||||||
DEFAULT_PAYLOAD_LOCK = "LOCK"
|
DEFAULT_PAYLOAD_LOCK = "LOCK"
|
||||||
@ -52,6 +53,7 @@ DEFAULT_STATE_LOCKED = "LOCKED"
|
|||||||
DEFAULT_STATE_LOCKING = "LOCKING"
|
DEFAULT_STATE_LOCKING = "LOCKING"
|
||||||
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
|
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
|
||||||
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
|
DEFAULT_STATE_UNLOCKING = "UNLOCKING"
|
||||||
|
DEFAULT_STATE_JAMMED = "JAMMED"
|
||||||
|
|
||||||
MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
|
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_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string,
|
||||||
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_JAMMED, default=DEFAULT_STATE_JAMMED): 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_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,
|
||||||
@ -83,6 +86,7 @@ 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 = [
|
STATE_CONFIG_KEYS = [
|
||||||
|
CONF_STATE_JAMMED,
|
||||||
CONF_STATE_LOCKED,
|
CONF_STATE_LOCKED,
|
||||||
CONF_STATE_LOCKING,
|
CONF_STATE_LOCKING,
|
||||||
CONF_STATE_UNLOCKED,
|
CONF_STATE_UNLOCKED,
|
||||||
@ -157,15 +161,20 @@ class MqttLock(MqttEntity, LockEntity):
|
|||||||
def _prepare_subscribe_topics(self) -> None:
|
def _prepare_subscribe_topics(self) -> None:
|
||||||
"""(Re)Subscribe to topics."""
|
"""(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
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
def message_received(msg: ReceiveMessage) -> None:
|
def message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle new MQTT messages."""
|
"""Handle new lock state messages."""
|
||||||
payload = self._value_template(msg.payload)
|
payload = self._value_template(msg.payload)
|
||||||
if payload in self._valid_states:
|
if payload in self._valid_states:
|
||||||
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
||||||
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
||||||
self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING]
|
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)
|
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||||
|
|
||||||
@ -173,18 +182,18 @@ class MqttLock(MqttEntity, LockEntity):
|
|||||||
# Force into optimistic mode.
|
# Force into optimistic mode.
|
||||||
self._optimistic = True
|
self._optimistic = True
|
||||||
else:
|
else:
|
||||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
topics[CONF_STATE_TOPIC] = {
|
||||||
self.hass,
|
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||||
self._sub_state,
|
"msg_callback": message_received,
|
||||||
{
|
CONF_QOS: qos,
|
||||||
"state_topic": {
|
CONF_ENCODING: encoding,
|
||||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
}
|
||||||
"msg_callback": message_received,
|
|
||||||
"qos": self._config[CONF_QOS],
|
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||||
"encoding": self._config[CONF_ENCODING] or None,
|
self.hass,
|
||||||
}
|
self._sub_state,
|
||||||
},
|
topics,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _subscribe_topics(self) -> None:
|
async def _subscribe_topics(self) -> None:
|
||||||
"""(Re)Subscribe to topics."""
|
"""(Re)Subscribe to topics."""
|
||||||
|
@ -8,6 +8,7 @@ from homeassistant.components.lock import (
|
|||||||
SERVICE_LOCK,
|
SERVICE_LOCK,
|
||||||
SERVICE_OPEN,
|
SERVICE_OPEN,
|
||||||
SERVICE_UNLOCK,
|
SERVICE_UNLOCK,
|
||||||
|
STATE_JAMMED,
|
||||||
STATE_LOCKED,
|
STATE_LOCKED,
|
||||||
STATE_LOCKING,
|
STATE_LOCKING,
|
||||||
STATE_UNLOCKED,
|
STATE_UNLOCKED,
|
||||||
@ -21,6 +22,7 @@ from homeassistant.const import (
|
|||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .test_common import (
|
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)
|
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(
|
async def test_availability_when_connection_lost(
|
||||||
hass, mqtt_mock_entry_with_yaml_config
|
hass, mqtt_mock_entry_with_yaml_config
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user