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

@ -11,6 +11,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
@ -76,6 +78,16 @@ class DemoLock(LockEntity):
"""Return true if lock is locked."""
return self._state == STATE_LOCKED
@property
def is_open(self) -> bool:
"""Return true if lock is open."""
return self._state == STATE_OPEN
@property
def is_opening(self) -> bool:
"""Return true if lock is opening."""
return self._state == STATE_OPENING
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
self._state = STATE_LOCKING
@ -97,5 +109,8 @@ class DemoLock(LockEntity):
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
self._state = STATE_UNLOCKED
self._state = STATE_OPENING
self.async_write_ha_state()
await asyncio.sleep(LOCK_UNLOCK_DELAY)
self._state = STATE_OPEN
self.async_write_ha_state()

View file

@ -25,6 +25,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_UNLOCKING,
@ -175,12 +177,16 @@ class LockGroup(GroupEntity, LockEntity):
# Set as unknown if any member is unknown or unavailable
self._attr_is_jammed = None
self._attr_is_locking = None
self._attr_is_opening = None
self._attr_is_open = None
self._attr_is_unlocking = None
self._attr_is_locked = None
else:
# Set attributes based on member states and let the lock entity sort out the correct state
self._attr_is_jammed = STATE_JAMMED in states
self._attr_is_locking = STATE_LOCKING in states
self._attr_is_opening = STATE_OPENING in states
self._attr_is_open = STATE_OPEN in states
self._attr_is_unlocking = STATE_UNLOCKING in states
self._attr_is_locked = all(state == STATE_LOCKED for state in states)

View file

@ -6,7 +6,7 @@ from typing import Any
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.const import STATE_LOCKED, STATE_OPEN, STATE_UNLOCKED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -79,6 +79,11 @@ class DemoLock(LockEntity):
"""Return true if lock is locked."""
return self._state == STATE_LOCKED
@property
def is_open(self) -> bool:
"""Return true if lock is open."""
return self._state == STATE_OPEN
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
self._attr_is_locking = True
@ -97,5 +102,5 @@ class DemoLock(LockEntity):
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
self._state = STATE_UNLOCKED
self._state = STATE_OPEN
self.async_write_ha_state()

View file

@ -22,6 +22,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
@ -121,6 +123,8 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"is_locked",
"is_locking",
"is_unlocking",
"is_open",
"is_opening",
"is_jammed",
"supported_features",
}
@ -134,6 +138,8 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_code_format: str | None = None
_attr_is_locked: bool | None = None
_attr_is_locking: bool | None = None
_attr_is_open: bool | None = None
_attr_is_opening: bool | None = None
_attr_is_unlocking: bool | None = None
_attr_is_jammed: bool | None = None
_attr_state: None = None
@ -202,6 +208,16 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return true if the lock is unlocking."""
return self._attr_is_unlocking
@cached_property
def is_open(self) -> bool | None:
"""Return true if the lock is open."""
return self._attr_is_open
@cached_property
def is_opening(self) -> bool | None:
"""Return true if the lock is opening."""
return self._attr_is_opening
@cached_property
def is_jammed(self) -> bool | None:
"""Return true if the lock is jammed (incomplete locking)."""
@ -262,8 +278,12 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the state."""
if self.is_jammed:
return STATE_JAMMED
if self.is_opening:
return STATE_OPENING
if self.is_locking:
return STATE_LOCKING
if self.is_open:
return STATE_OPEN
if self.is_unlocking:
return STATE_UNLOCKING
if (locked := self.is_locked) is None:

View file

@ -14,6 +14,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
@ -31,11 +33,13 @@ from . import DOMAIN
# mypy: disallow-any-generics
CONDITION_TYPES = {
"is_locked",
"is_unlocked",
"is_locking",
"is_unlocking",
"is_jammed",
"is_locked",
"is_locking",
"is_open",
"is_opening",
"is_unlocked",
"is_unlocking",
}
CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
@ -78,8 +82,12 @@ def async_condition_from_config(
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_jammed":
state = STATE_JAMMED
elif config[CONF_TYPE] == "is_opening":
state = STATE_OPENING
elif config[CONF_TYPE] == "is_locking":
state = STATE_LOCKING
elif config[CONF_TYPE] == "is_open":
state = STATE_OPEN
elif config[CONF_TYPE] == "is_unlocking":
state = STATE_UNLOCKING
elif config[CONF_TYPE] == "is_locked":

View file

@ -16,6 +16,8 @@ from homeassistant.const import (
STATE_JAMMED,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
@ -26,7 +28,15 @@ from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
TRIGGER_TYPES = {"locked", "unlocked", "locking", "unlocking", "jammed"}
TRIGGER_TYPES = {
"jammed",
"locked",
"locking",
"open",
"opening",
"unlocked",
"unlocking",
}
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
@ -84,8 +94,12 @@ async def async_attach_trigger(
"""Attach a trigger."""
if config[CONF_TYPE] == "jammed":
to_state = STATE_JAMMED
elif config[CONF_TYPE] == "opening":
to_state = STATE_OPENING
elif config[CONF_TYPE] == "locking":
to_state = STATE_LOCKING
elif config[CONF_TYPE] == "open":
to_state = STATE_OPEN
elif config[CONF_TYPE] == "unlocking":
to_state = STATE_UNLOCKING
elif config[CONF_TYPE] == "locked":

View file

@ -2,7 +2,14 @@
from typing import TYPE_CHECKING
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.const import (
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
@ -16,4 +23,15 @@ def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None:
"""Describe group on off states."""
registry.on_off_states(DOMAIN, {STATE_UNLOCKED}, STATE_UNLOCKED, STATE_LOCKED)
registry.on_off_states(
DOMAIN,
{
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
},
STATE_UNLOCKED,
STATE_LOCKED,
)

View file

@ -5,6 +5,8 @@
"state": {
"jammed": "mdi:lock-alert",
"locking": "mdi:lock-clock",
"open": "mdi:lock-open-variant",
"opening": "mdi:lock-clock",
"unlocked": "mdi:lock-open-variant",
"unlocking": "mdi:lock-clock"
}

View file

@ -10,9 +10,12 @@ from typing import Any
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
)
@ -22,7 +25,14 @@ from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED, STATE_LOCKING, STATE_UNLOCKING}
VALID_STATES = {
STATE_LOCKED,
STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED,
STATE_UNLOCKING,
}
async def _async_reproduce_state(
@ -53,6 +63,8 @@ async def _async_reproduce_state(
service = SERVICE_LOCK
elif state.state in {STATE_UNLOCKED, STATE_UNLOCKING}:
service = SERVICE_UNLOCK
elif state.state in {STATE_OPEN, STATE_OPENING}:
service = SERVICE_OPEN
await hass.services.async_call(
DOMAIN, service, service_data, context=context, blocking=True

View file

@ -8,11 +8,13 @@
},
"condition_type": {
"is_locked": "{entity_name} is locked",
"is_unlocked": "{entity_name} is unlocked"
"is_unlocked": "{entity_name} is unlocked",
"is_open": "{entity_name} is open"
},
"trigger_type": {
"locked": "{entity_name} locked",
"unlocked": "{entity_name} unlocked"
"unlocked": "{entity_name} unlocked",
"open": "{entity_name} opened"
}
},
"entity_component": {
@ -22,6 +24,8 @@
"jammed": "Jammed",
"locked": "[%key:common::state::locked%]",
"locking": "Locking",
"open": "[%key:common::state::open%]",
"opening": "Opening",
"unlocked": "[%key:common::state::unlocked%]",
"unlocking": "Unlocking"
},

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"}