diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 7a974e44b27..bada22a6544 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -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", diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index f56dba6766a..a8d8a3df668 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -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) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef1690221aa..20079e3c1f7 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -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(