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_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, STATE_UNLOCKING,
) )
@ -76,6 +78,16 @@ class DemoLock(LockEntity):
"""Return true if lock is locked.""" """Return true if lock is locked."""
return self._state == STATE_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: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device.""" """Lock the device."""
self._state = STATE_LOCKING self._state = STATE_LOCKING
@ -97,5 +109,8 @@ class DemoLock(LockEntity):
async def async_open(self, **kwargs: Any) -> None: async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch.""" """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() self.async_write_ha_state()

View file

@ -25,6 +25,8 @@ from homeassistant.const import (
STATE_JAMMED, STATE_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKING, STATE_UNLOCKING,
@ -175,12 +177,16 @@ class LockGroup(GroupEntity, LockEntity):
# Set as unknown if any member is unknown or unavailable # Set as unknown if any member is unknown or unavailable
self._attr_is_jammed = None self._attr_is_jammed = None
self._attr_is_locking = None self._attr_is_locking = None
self._attr_is_opening = None
self._attr_is_open = None
self._attr_is_unlocking = None self._attr_is_unlocking = None
self._attr_is_locked = None self._attr_is_locked = None
else: else:
# Set attributes based on member states and let the lock entity sort out the correct state # 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_jammed = STATE_JAMMED in states
self._attr_is_locking = STATE_LOCKING 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_unlocking = STATE_UNLOCKING in states
self._attr_is_locked = all(state == STATE_LOCKED for state 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.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -79,6 +79,11 @@ class DemoLock(LockEntity):
"""Return true if lock is locked.""" """Return true if lock is locked."""
return self._state == STATE_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: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device.""" """Lock the device."""
self._attr_is_locking = True self._attr_is_locking = True
@ -97,5 +102,5 @@ class DemoLock(LockEntity):
async def async_open(self, **kwargs: Any) -> None: async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch.""" """Open the door latch."""
self._state = STATE_UNLOCKED self._state = STATE_OPEN
self.async_write_ha_state() self.async_write_ha_state()

View file

@ -22,6 +22,8 @@ from homeassistant.const import (
STATE_JAMMED, STATE_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, STATE_UNLOCKING,
) )
@ -121,6 +123,8 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"is_locked", "is_locked",
"is_locking", "is_locking",
"is_unlocking", "is_unlocking",
"is_open",
"is_opening",
"is_jammed", "is_jammed",
"supported_features", "supported_features",
} }
@ -134,6 +138,8 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_code_format: str | None = None _attr_code_format: str | None = None
_attr_is_locked: bool | None = None _attr_is_locked: bool | None = None
_attr_is_locking: 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_unlocking: bool | None = None
_attr_is_jammed: bool | None = None _attr_is_jammed: bool | None = None
_attr_state: 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 true if the lock is unlocking."""
return self._attr_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 @cached_property
def is_jammed(self) -> bool | None: def is_jammed(self) -> bool | None:
"""Return true if the lock is jammed (incomplete locking).""" """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.""" """Return the state."""
if self.is_jammed: if self.is_jammed:
return STATE_JAMMED return STATE_JAMMED
if self.is_opening:
return STATE_OPENING
if self.is_locking: if self.is_locking:
return STATE_LOCKING return STATE_LOCKING
if self.is_open:
return STATE_OPEN
if self.is_unlocking: if self.is_unlocking:
return STATE_UNLOCKING return STATE_UNLOCKING
if (locked := self.is_locked) is None: if (locked := self.is_locked) is None:

View file

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

View file

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

View file

@ -2,7 +2,14 @@
from typing import TYPE_CHECKING 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 homeassistant.core import HomeAssistant, callback
from .const import DOMAIN from .const import DOMAIN
@ -16,4 +23,15 @@ def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry" hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None: ) -> None:
"""Describe group on off states.""" """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": { "state": {
"jammed": "mdi:lock-alert", "jammed": "mdi:lock-alert",
"locking": "mdi:lock-clock", "locking": "mdi:lock-clock",
"open": "mdi:lock-open-variant",
"opening": "mdi:lock-clock",
"unlocked": "mdi:lock-open-variant", "unlocked": "mdi:lock-open-variant",
"unlocking": "mdi:lock-clock" "unlocking": "mdi:lock-clock"
} }

View file

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

View file

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

View file

@ -16,7 +16,13 @@ from homeassistant.components.lock import (
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, 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.core import HomeAssistant
from homeassistant.setup import async_setup_component 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 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) @patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0)
async def test_jammed_when_locking(hass: HomeAssistant) -> None: async def test_jammed_when_locking(hass: HomeAssistant) -> None:
"""Test the locking of a lock jams.""" """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 LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True
) )
assert len(calls) == 1 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, SERVICE_RELOAD,
STATE_CLOSED, STATE_CLOSED,
STATE_HOME, STATE_HOME,
STATE_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING,
STATE_NOT_HOME, STATE_NOT_HOME,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_OPEN, STATE_OPEN,
STATE_OPENING,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING,
) )
from homeassistant.core import CoreState, HomeAssistant from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -769,6 +773,48 @@ async def test_is_on(hass: HomeAssistant) -> None:
(STATE_ON, True), (STATE_ON, True),
(STATE_OFF, False), (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( 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", "unlocked"), "unlocked"),
(("locked", "locked", "locked"), "locked"), (("locked", "locked", "locked"), "locked"),
(("locked", "locked", "open"), "unlocked"),
(("locked", "unlocked", "open"), "unlocked"),
], ],
) )
async def test_group_locks(hass: HomeAssistant, states, group_state) -> None: async def test_group_locks(hass: HomeAssistant, states, group_state) -> None:

View file

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

View file

@ -16,7 +16,12 @@ from homeassistant.components.lock import (
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, 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.core import HomeAssistant
from homeassistant.setup import async_setup_component 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 LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: OPENABLE_LOCK}, blocking=True
) )
state = hass.states.get(OPENABLE_LOCK) 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_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, STATE_UNLOCKING,
EntityCategory, EntityCategory,
@ -32,7 +34,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
@pytest.fixture @pytest.fixture
def calls(hass): def calls(hass: HomeAssistant):
"""Track calls to a mock service.""" """Track calls to a mock service."""
return async_mock_service(hass, "test", "automation") return async_mock_service(hass, "test", "automation")
@ -67,6 +69,8 @@ async def test_get_conditions(
"is_unlocking", "is_unlocking",
"is_locking", "is_locking",
"is_jammed", "is_jammed",
"is_open",
"is_opening",
] ]
] ]
conditions = await async_get_device_automations( conditions = await async_get_device_automations(
@ -121,6 +125,8 @@ async def test_get_conditions_hidden_auxiliary(
"is_unlocking", "is_unlocking",
"is_locking", "is_locking",
"is_jammed", "is_jammed",
"is_open",
"is_opening",
] ]
] ]
conditions = await async_get_device_automations( 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 len(calls) == 5
assert calls[4].data["some"] == "is_jammed - event - test_event5" 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( async def test_if_state_legacy(
hass: HomeAssistant, hass: HomeAssistant,

View file

@ -7,11 +7,13 @@ from pytest_unordered import unordered
from homeassistant.components import automation from homeassistant.components import automation
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.lock import DOMAIN from homeassistant.components.lock import DOMAIN, LockEntityFeature
from homeassistant.const import ( from homeassistant.const import (
STATE_JAMMED, STATE_JAMMED,
STATE_LOCKED, STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_OPEN,
STATE_OPENING,
STATE_UNLOCKED, STATE_UNLOCKED,
STATE_UNLOCKING, STATE_UNLOCKING,
EntityCategory, EntityCategory,
@ -37,7 +39,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
@pytest.fixture @pytest.fixture
def calls(hass): def calls(hass: HomeAssistant):
"""Track calls to a mock service.""" """Track calls to a mock service."""
return async_mock_service(hass, "test", "automation") 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")}, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
) )
entity_entry = entity_registry.async_get_or_create( 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 = [ expected_triggers = [
{ {
@ -66,7 +72,15 @@ async def test_get_triggers(
"entity_id": entity_entry.id, "entity_id": entity_entry.id,
"metadata": {"secondary": False}, "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( triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id hass, DeviceAutomationType.TRIGGER, device_entry.id
@ -104,6 +118,7 @@ async def test_get_triggers_hidden_auxiliary(
device_id=device_entry.id, device_id=device_entry.id,
entity_category=entity_category, entity_category=entity_category,
hidden_by=hidden_by, hidden_by=hidden_by,
supported_features=LockEntityFeature.OPEN,
) )
expected_triggers = [ expected_triggers = [
{ {
@ -114,7 +129,15 @@ async def test_get_triggers_hidden_auxiliary(
"entity_id": entity_entry.id, "entity_id": entity_entry.id,
"metadata": {"secondary": True}, "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( triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id hass, DeviceAutomationType.TRIGGER, device_entry.id
@ -141,7 +164,7 @@ async def test_get_trigger_capabilities(
triggers = await async_get_device_automations( triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id hass, DeviceAutomationType.TRIGGER, device_entry.id
) )
assert len(triggers) == 5 assert len(triggers) == 7
for trigger in triggers: for trigger in triggers:
capabilities = await async_get_device_automation_capabilities( capabilities = await async_get_device_automation_capabilities(
hass, DeviceAutomationType.TRIGGER, trigger hass, DeviceAutomationType.TRIGGER, trigger
@ -172,7 +195,7 @@ async def test_get_trigger_capabilities_legacy(
triggers = await async_get_device_automations( triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id hass, DeviceAutomationType.TRIGGER, device_entry.id
) )
assert len(triggers) == 5 assert len(triggers) == 7
for trigger in triggers: for trigger in triggers:
trigger["entity_id"] = entity_registry.async_get(trigger["entity_id"]).entity_id trigger["entity_id"] = entity_registry.async_get(trigger["entity_id"]).entity_id
capabilities = await async_get_device_automation_capabilities( 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" == 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( async def test_if_fires_on_state_change_legacy(
hass: HomeAssistant, 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"] calls[3].data["some"]
== f"turn_on device - {entry.entity_id} - jammed - locking - 0:00:05" == 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, STATE_UNLOCKING,
LockEntityFeature, LockEntityFeature,
) )
from homeassistant.const import STATE_OPEN, STATE_OPENING
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.entity_registry as er 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_locked is None
assert mock_lock_entity.is_locking is None assert mock_lock_entity.is_locking is None
assert mock_lock_entity.is_unlocking 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: 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 mock_lock_entity.state == STATE_JAMMED
assert not mock_lock_entity.is_locked 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( @pytest.mark.parametrize(
("code_format", "supported_features"), ("code_format", "supported_features"),

View file

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