Add open state to LockEntity (#111968)

* Add `open` state to LockEntity

* Add tests

* Fixes

* Fix tests

* strings and icons

* Adjust demo open lock

* Fix lock and tests

* fix import

* Fix strings

* mute ruff

* Change sequence

* Sequence2

* Group on states

* Fix ruff

* Fix tests

* Add more test cases

* Sorting
This commit is contained in:
G Johansson 2024-05-08 20:42:22 +02:00 committed by GitHub
parent 189c07d502
commit 7862596ef3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 377 additions and 37 deletions

View file

@ -16,7 +16,13 @@ from homeassistant.components.lock import (
STATE_UNLOCKED,
STATE_UNLOCKING,
)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform
from homeassistant.const import (
ATTR_ENTITY_ID,
EVENT_STATE_CHANGED,
STATE_OPEN,
STATE_OPENING,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -87,6 +93,26 @@ async def test_unlocking(hass: HomeAssistant) -> None:
assert state_changes[1].data["new_state"].state == STATE_UNLOCKED
@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0)
async def test_opening(hass: HomeAssistant) -> None:
"""Test the opening of a lock."""
state = hass.states.get(OPENABLE_LOCK)
assert state.state == STATE_LOCKED
await hass.async_block_till_done()
state_changes = async_capture_events(hass, EVENT_STATE_CHANGED)
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=False
)
await hass.async_block_till_done()
assert state_changes[0].data["entity_id"] == OPENABLE_LOCK
assert state_changes[0].data["new_state"].state == STATE_OPENING
assert state_changes[1].data["entity_id"] == OPENABLE_LOCK
assert state_changes[1].data["new_state"].state == STATE_OPEN
@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0)
async def test_jammed_when_locking(hass: HomeAssistant) -> None:
"""Test the locking of a lock jams."""
@ -114,12 +140,3 @@ async def test_opening_mocked(hass: HomeAssistant) -> None:
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True
)
assert len(calls) == 1
async def test_opening(hass: HomeAssistant) -> None:
"""Test the opening of a lock."""
await hass.services.async_call(
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True
)
state = hass.states.get(OPENABLE_LOCK)
assert state.state == STATE_UNLOCKED

View file

@ -19,13 +19,17 @@ from homeassistant.const import (
SERVICE_RELOAD,
STATE_CLOSED,
STATE_HOME,
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_NOT_HOME,
STATE_OFF,
STATE_ON,
STATE_OPEN,
STATE_OPENING,
STATE_UNKNOWN,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -769,6 +773,48 @@ async def test_is_on(hass: HomeAssistant) -> None:
(STATE_ON, True),
(STATE_OFF, False),
),
(
("lock", "lock"),
(STATE_OPEN, STATE_LOCKED),
(STATE_LOCKED, STATE_LOCKED),
(STATE_UNLOCKED, True),
(STATE_LOCKED, False),
),
(
("lock", "lock"),
(STATE_OPENING, STATE_LOCKED),
(STATE_LOCKED, STATE_LOCKED),
(STATE_UNLOCKED, True),
(STATE_LOCKED, False),
),
(
("lock", "lock"),
(STATE_UNLOCKING, STATE_LOCKED),
(STATE_LOCKED, STATE_LOCKED),
(STATE_UNLOCKED, True),
(STATE_LOCKED, False),
),
(
("lock", "lock"),
(STATE_LOCKING, STATE_LOCKED),
(STATE_LOCKED, STATE_LOCKED),
(STATE_UNLOCKED, True),
(STATE_LOCKED, False),
),
(
("lock", "lock"),
(STATE_JAMMED, STATE_LOCKED),
(STATE_LOCKED, STATE_LOCKED),
(STATE_LOCKED, False),
(STATE_LOCKED, False),
),
(
("cover", "lock"),
(STATE_OPEN, STATE_OPEN),
(STATE_CLOSED, STATE_LOCKED),
(STATE_ON, True),
(STATE_OFF, False),
),
],
)
async def test_is_on_and_state_mixed_domains(
@ -1247,6 +1293,8 @@ async def test_group_mixed_domains_off(hass: HomeAssistant) -> None:
[
(("locked", "locked", "unlocked"), "unlocked"),
(("locked", "locked", "locked"), "locked"),
(("locked", "locked", "open"), "unlocked"),
(("locked", "unlocked", "open"), "unlocked"),
],
)
async def test_group_locks(hass: HomeAssistant, states, group_state) -> None:

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_UNLOCKED,
@ -204,8 +205,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: "lock.lock_group"},
blocking=True,
)
assert hass.states.get("lock.openable_lock").state == STATE_UNLOCKED
assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED
assert hass.states.get("lock.openable_lock").state == STATE_OPEN
assert hass.states.get("lock.another_openable_lock").state == STATE_OPEN
await hass.services.async_call(
LOCK_DOMAIN,

View file

@ -16,7 +16,12 @@ from homeassistant.components.lock import (
STATE_UNLOCKED,
STATE_UNLOCKING,
)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform
from homeassistant.const import (
ATTR_ENTITY_ID,
EVENT_STATE_CHANGED,
STATE_OPEN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -103,4 +108,4 @@ async def test_opening(hass: HomeAssistant) -> None:
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True
)
state = hass.states.get(OPENABLE_LOCK)
assert state.state == STATE_UNLOCKED
assert state.state == STATE_OPEN

View file

@ -10,6 +10,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
EntityCategory,
@ -32,7 +34,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
@pytest.fixture
def calls(hass):
def calls(hass: HomeAssistant):
"""Track calls to a mock service."""
return async_mock_service(hass, "test", "automation")
@ -67,6 +69,8 @@ async def test_get_conditions(
"is_unlocking",
"is_locking",
"is_jammed",
"is_open",
"is_opening",
]
]
conditions = await async_get_device_automations(
@ -121,6 +125,8 @@ async def test_get_conditions_hidden_auxiliary(
"is_unlocking",
"is_locking",
"is_jammed",
"is_open",
"is_opening",
]
]
conditions = await async_get_device_automations(
@ -243,6 +249,42 @@ async def test_if_state(
},
},
},
{
"trigger": {"platform": "event", "event_type": "test_event6"},
"condition": [
{
"condition": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"entity_id": entry.id,
"type": "is_opening",
}
],
"action": {
"service": "test.automation",
"data_template": {
"some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}"
},
},
},
{
"trigger": {"platform": "event", "event_type": "test_event7"},
"condition": [
{
"condition": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"entity_id": entry.id,
"type": "is_open",
}
],
"action": {
"service": "test.automation",
"data_template": {
"some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}"
},
},
},
]
},
)
@ -277,6 +319,18 @@ async def test_if_state(
assert len(calls) == 5
assert calls[4].data["some"] == "is_jammed - event - test_event5"
hass.states.async_set(entry.entity_id, STATE_OPENING)
hass.bus.async_fire("test_event6")
await hass.async_block_till_done()
assert len(calls) == 6
assert calls[5].data["some"] == "is_opening - event - test_event6"
hass.states.async_set(entry.entity_id, STATE_OPEN)
hass.bus.async_fire("test_event7")
await hass.async_block_till_done()
assert len(calls) == 7
assert calls[6].data["some"] == "is_open - event - test_event7"
async def test_if_state_legacy(
hass: HomeAssistant,

View file

@ -7,11 +7,13 @@ from pytest_unordered import unordered
from homeassistant.components import automation
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.lock import DOMAIN
from homeassistant.components.lock import DOMAIN, LockEntityFeature
from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
EntityCategory,
@ -37,7 +39,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
@pytest.fixture
def calls(hass):
def calls(hass: HomeAssistant):
"""Track calls to a mock service."""
return async_mock_service(hass, "test", "automation")
@ -55,7 +57,11 @@ async def test_get_triggers(
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
DOMAIN, "test", "5678", device_id=device_entry.id
DOMAIN,
"test",
"5678",
device_id=device_entry.id,
supported_features=LockEntityFeature.OPEN,
)
expected_triggers = [
{
@ -66,7 +72,15 @@ async def test_get_triggers(
"entity_id": entity_entry.id,
"metadata": {"secondary": False},
}
for trigger in ["locked", "unlocked", "unlocking", "locking", "jammed"]
for trigger in [
"locked",
"unlocked",
"unlocking",
"locking",
"jammed",
"open",
"opening",
]
]
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
@ -104,6 +118,7 @@ async def test_get_triggers_hidden_auxiliary(
device_id=device_entry.id,
entity_category=entity_category,
hidden_by=hidden_by,
supported_features=LockEntityFeature.OPEN,
)
expected_triggers = [
{
@ -114,7 +129,15 @@ async def test_get_triggers_hidden_auxiliary(
"entity_id": entity_entry.id,
"metadata": {"secondary": True},
}
for trigger in ["locked", "unlocked", "unlocking", "locking", "jammed"]
for trigger in [
"locked",
"unlocked",
"unlocking",
"locking",
"jammed",
"open",
"opening",
]
]
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
@ -141,7 +164,7 @@ async def test_get_trigger_capabilities(
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
)
assert len(triggers) == 5
assert len(triggers) == 7
for trigger in triggers:
capabilities = await async_get_device_automation_capabilities(
hass, DeviceAutomationType.TRIGGER, trigger
@ -172,7 +195,7 @@ async def test_get_trigger_capabilities_legacy(
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
)
assert len(triggers) == 5
assert len(triggers) == 7
for trigger in triggers:
trigger["entity_id"] = entity_registry.async_get(trigger["entity_id"]).entity_id
capabilities = await async_get_device_automation_capabilities(
@ -247,6 +270,25 @@ async def test_if_fires_on_state_change(
},
},
},
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"entity_id": entry.id,
"type": "open",
},
"action": {
"service": "test.automation",
"data_template": {
"some": (
"open - {{ trigger.platform}} - "
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
"{{ trigger.to_state.state}} - {{ trigger.for }}"
)
},
},
},
]
},
)
@ -269,6 +311,15 @@ async def test_if_fires_on_state_change(
== f"unlocked - device - {entry.entity_id} - locked - unlocked - None"
)
# Fake that the entity is opens.
hass.states.async_set(entry.entity_id, STATE_OPEN)
await hass.async_block_till_done()
assert len(calls) == 3
assert (
calls[2].data["some"]
== f"open - device - {entry.entity_id} - unlocked - open - None"
)
async def test_if_fires_on_state_change_legacy(
hass: HomeAssistant,
@ -439,6 +490,28 @@ async def test_if_fires_on_state_change_with_for(
},
},
},
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"entity_id": entry.id,
"type": "opening",
"for": {"seconds": 5},
},
"action": {
"service": "test.automation",
"data_template": {
"some": (
"turn_on {{ trigger.platform }}"
" - {{ trigger.entity_id }}"
" - {{ trigger.from_state.state }}"
" - {{ trigger.to_state.state }}"
" - {{ trigger.for }}"
)
},
},
},
]
},
)
@ -492,3 +565,15 @@ async def test_if_fires_on_state_change_with_for(
calls[3].data["some"]
== f"turn_on device - {entry.entity_id} - jammed - locking - 0:00:05"
)
hass.states.async_set(entry.entity_id, STATE_OPENING)
await hass.async_block_till_done()
assert len(calls) == 4
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=27))
await hass.async_block_till_done()
assert len(calls) == 5
await hass.async_block_till_done()
assert (
calls[4].data["some"]
== f"turn_on device - {entry.entity_id} - locking - opening - 0:00:05"
)

View file

@ -22,6 +22,7 @@ from homeassistant.components.lock import (
STATE_UNLOCKING,
LockEntityFeature,
)
from homeassistant.const import STATE_OPEN, STATE_OPENING
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.entity_registry as er
@ -55,6 +56,8 @@ async def test_lock_default(hass: HomeAssistant, mock_lock_entity: MockLock) ->
assert mock_lock_entity.is_locked is None
assert mock_lock_entity.is_locking is None
assert mock_lock_entity.is_unlocking is None
assert mock_lock_entity.is_opening is None
assert mock_lock_entity.is_open is None
async def test_lock_states(hass: HomeAssistant, mock_lock_entity: MockLock) -> None:
@ -85,6 +88,19 @@ async def test_lock_states(hass: HomeAssistant, mock_lock_entity: MockLock) -> N
assert mock_lock_entity.state == STATE_JAMMED
assert not mock_lock_entity.is_locked
mock_lock_entity._attr_is_jammed = False
mock_lock_entity._attr_is_opening = True
assert mock_lock_entity.is_opening
assert mock_lock_entity.state == STATE_OPENING
assert mock_lock_entity.is_opening
mock_lock_entity._attr_is_opening = False
mock_lock_entity._attr_is_open = True
assert not mock_lock_entity.is_opening
assert mock_lock_entity.state == STATE_OPEN
assert not mock_lock_entity.is_opening
assert mock_lock_entity.is_open
@pytest.mark.parametrize(
("code_format", "supported_features"),

View file

@ -14,9 +14,11 @@ async def test_reproducing_states(
"""Test reproducing Lock states."""
hass.states.async_set("lock.entity_locked", "locked", {})
hass.states.async_set("lock.entity_unlocked", "unlocked", {})
hass.states.async_set("lock.entity_opened", "open", {})
lock_calls = async_mock_service(hass, "lock", "lock")
unlock_calls = async_mock_service(hass, "lock", "unlock")
open_calls = async_mock_service(hass, "lock", "open")
# These calls should do nothing as entities already in desired state
await async_reproduce_state(
@ -24,11 +26,13 @@ async def test_reproducing_states(
[
State("lock.entity_locked", "locked"),
State("lock.entity_unlocked", "unlocked", {}),
State("lock.entity_opened", "open", {}),
],
)
assert len(lock_calls) == 0
assert len(unlock_calls) == 0
assert len(open_calls) == 0
# Test invalid state is handled
await async_reproduce_state(hass, [State("lock.entity_locked", "not_supported")])
@ -36,13 +40,15 @@ async def test_reproducing_states(
assert "not_supported" in caplog.text
assert len(lock_calls) == 0
assert len(unlock_calls) == 0
assert len(open_calls) == 0
# Make sure correct services are called
await async_reproduce_state(
hass,
[
State("lock.entity_locked", "unlocked"),
State("lock.entity_locked", "open"),
State("lock.entity_unlocked", "locked"),
State("lock.entity_opened", "unlocked"),
# Should not raise
State("lock.non_existing", "on"),
],
@ -54,4 +60,8 @@ async def test_reproducing_states(
assert len(unlock_calls) == 1
assert unlock_calls[0].domain == "lock"
assert unlock_calls[0].data == {"entity_id": "lock.entity_locked"}
assert unlock_calls[0].data == {"entity_id": "lock.entity_opened"}
assert len(open_calls) == 1
assert open_calls[0].domain == "lock"
assert open_calls[0].data == {"entity_id": "lock.entity_locked"}