mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Lock entity options (#88139)
This commit is contained in:
parent
cdc4b315e5
commit
0bda869553
@ -24,7 +24,7 @@ from homeassistant.const import (
|
|||||||
STATE_UNLOCKED,
|
STATE_UNLOCKED,
|
||||||
STATE_UNLOCKING,
|
STATE_UNLOCKING,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
@ -39,6 +39,7 @@ from homeassistant.helpers.typing import ConfigType, StateType
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_CHANGED_BY = "changed_by"
|
ATTR_CHANGED_BY = "changed_by"
|
||||||
|
CONF_DEFAULT_CODE = "default_code"
|
||||||
|
|
||||||
DOMAIN = "lock"
|
DOMAIN = "lock"
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
@ -88,7 +89,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None:
|
async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None:
|
||||||
"""Lock the lock."""
|
"""Lock the lock."""
|
||||||
code: str = service_call.data.get(ATTR_CODE, "")
|
code: str = service_call.data.get(
|
||||||
|
ATTR_CODE, entity._lock_option_default_code # pylint: disable=protected-access
|
||||||
|
)
|
||||||
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Code '{code}' for locking {entity.entity_id} doesn't match pattern {entity.code_format}"
|
f"Code '{code}' for locking {entity.entity_id} doesn't match pattern {entity.code_format}"
|
||||||
@ -98,7 +101,9 @@ async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None:
|
|||||||
|
|
||||||
async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
|
async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
|
||||||
"""Unlock the lock."""
|
"""Unlock the lock."""
|
||||||
code: str = service_call.data.get(ATTR_CODE, "")
|
code: str = service_call.data.get(
|
||||||
|
ATTR_CODE, entity._lock_option_default_code # pylint: disable=protected-access
|
||||||
|
)
|
||||||
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Code '{code}' for unlocking {entity.entity_id} doesn't match pattern {entity.code_format}"
|
f"Code '{code}' for unlocking {entity.entity_id} doesn't match pattern {entity.code_format}"
|
||||||
@ -108,7 +113,9 @@ async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
|
|||||||
|
|
||||||
async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
|
async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
|
||||||
"""Open the door latch."""
|
"""Open the door latch."""
|
||||||
code: str = service_call.data.get(ATTR_CODE, "")
|
code: str = service_call.data.get(
|
||||||
|
ATTR_CODE, entity._lock_option_default_code # pylint: disable=protected-access
|
||||||
|
)
|
||||||
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
if entity.code_format_cmp and not entity.code_format_cmp.match(code):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Code '{code}' for opening {entity.entity_id} doesn't match pattern {entity.code_format}"
|
f"Code '{code}' for opening {entity.entity_id} doesn't match pattern {entity.code_format}"
|
||||||
@ -145,6 +152,7 @@ class LockEntity(Entity):
|
|||||||
_attr_is_jammed: bool | None = None
|
_attr_is_jammed: bool | None = None
|
||||||
_attr_state: None = None
|
_attr_state: None = None
|
||||||
_attr_supported_features: LockEntityFeature = LockEntityFeature(0)
|
_attr_supported_features: LockEntityFeature = LockEntityFeature(0)
|
||||||
|
_lock_option_default_code: str = ""
|
||||||
__code_format_cmp: re.Pattern[str] | None = None
|
__code_format_cmp: re.Pattern[str] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -243,3 +251,34 @@ class LockEntity(Entity):
|
|||||||
def supported_features(self) -> LockEntityFeature:
|
def supported_features(self) -> LockEntityFeature:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
return self._attr_supported_features
|
return self._attr_supported_features
|
||||||
|
|
||||||
|
async def async_internal_added_to_hass(self) -> None:
|
||||||
|
"""Call when the sensor entity is added to hass."""
|
||||||
|
await super().async_internal_added_to_hass()
|
||||||
|
if not self.registry_entry:
|
||||||
|
return
|
||||||
|
self._async_read_entity_options()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_registry_entry_updated(self) -> None:
|
||||||
|
"""Run when the entity registry entry has been updated."""
|
||||||
|
self._async_read_entity_options()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_read_entity_options(self) -> None:
|
||||||
|
"""Read entity options from entity registry.
|
||||||
|
|
||||||
|
Called when the entity registry entry has been updated and before the lock is
|
||||||
|
added to the state machine.
|
||||||
|
"""
|
||||||
|
assert self.registry_entry
|
||||||
|
if (lock_options := self.registry_entry.options.get(DOMAIN)) and (
|
||||||
|
custom_default_lock_code := lock_options.get(CONF_DEFAULT_CODE)
|
||||||
|
):
|
||||||
|
if self.code_format_cmp and self.code_format_cmp.match(
|
||||||
|
custom_default_lock_code
|
||||||
|
):
|
||||||
|
self._lock_option_default_code = custom_default_lock_code
|
||||||
|
return
|
||||||
|
|
||||||
|
self._lock_option_default_code = ""
|
||||||
|
@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.components.lock import (
|
from homeassistant.components.lock import (
|
||||||
ATTR_CODE,
|
ATTR_CODE,
|
||||||
|
CONF_DEFAULT_CODE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_LOCK,
|
SERVICE_LOCK,
|
||||||
SERVICE_OPEN,
|
SERVICE_OPEN,
|
||||||
@ -24,6 +25,10 @@ from homeassistant.components.lock import (
|
|||||||
_async_unlock,
|
_async_unlock,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.testing_config.custom_components.test.lock import MockLock
|
||||||
|
|
||||||
|
|
||||||
class MockLockEntity(LockEntity):
|
class MockLockEntity(LockEntity):
|
||||||
@ -32,6 +37,7 @@ class MockLockEntity(LockEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
code_format: str | None = None,
|
code_format: str | None = None,
|
||||||
|
lock_option_default_code: str = "",
|
||||||
supported_features: LockEntityFeature = LockEntityFeature(0),
|
supported_features: LockEntityFeature = LockEntityFeature(0),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize mock lock entity."""
|
"""Initialize mock lock entity."""
|
||||||
@ -39,6 +45,7 @@ class MockLockEntity(LockEntity):
|
|||||||
self.calls_open = MagicMock()
|
self.calls_open = MagicMock()
|
||||||
if code_format is not None:
|
if code_format is not None:
|
||||||
self._attr_code_format = code_format
|
self._attr_code_format = code_format
|
||||||
|
self._lock_option_default_code = lock_option_default_code
|
||||||
|
|
||||||
async def async_lock(self, **kwargs: Any) -> None:
|
async def async_lock(self, **kwargs: Any) -> None:
|
||||||
"""Lock the lock."""
|
"""Lock the lock."""
|
||||||
@ -95,6 +102,80 @@ async def test_lock_states(hass: HomeAssistant) -> None:
|
|||||||
assert not lock.is_locked
|
assert not lock.is_locked
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_default_code_option(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
enable_custom_integrations: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test default code stored in the registry."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
entry = entity_registry.async_get_or_create("lock", "test", "very_unique")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
platform = getattr(hass.components, "test.lock")
|
||||||
|
platform.init(empty=True)
|
||||||
|
platform.ENTITIES["lock1"] = platform.MockLock(
|
||||||
|
name="Test",
|
||||||
|
code_format=r"^\d{4}$",
|
||||||
|
supported_features=LockEntityFeature.OPEN,
|
||||||
|
unique_id="very_unique",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "lock", {"lock": {"platform": "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity0: MockLock = platform.ENTITIES["lock1"]
|
||||||
|
entity_registry.async_update_entity_options(
|
||||||
|
entry.entity_id, "lock", {CONF_DEFAULT_CODE: "1234"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity0._lock_option_default_code == "1234"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_default_code_option_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
enable_custom_integrations: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test default code stored in the registry is updated."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
entry = entity_registry.async_get_or_create("lock", "test", "very_unique")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
platform = getattr(hass.components, "test.lock")
|
||||||
|
platform.init(empty=True)
|
||||||
|
|
||||||
|
# Pre-register entities
|
||||||
|
entry = entity_registry.async_get_or_create("lock", "test", "very_unique")
|
||||||
|
entity_registry.async_update_entity_options(
|
||||||
|
entry.entity_id,
|
||||||
|
"lock",
|
||||||
|
{
|
||||||
|
"default_code": "5432",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
platform.ENTITIES["lock1"] = platform.MockLock(
|
||||||
|
name="Test",
|
||||||
|
code_format=r"^\d{4}$",
|
||||||
|
supported_features=LockEntityFeature.OPEN,
|
||||||
|
unique_id="very_unique",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "lock", {"lock": {"platform": "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity0: MockLock = platform.ENTITIES["lock1"]
|
||||||
|
assert entity0._lock_option_default_code == "5432"
|
||||||
|
|
||||||
|
entity_registry.async_update_entity_options(
|
||||||
|
entry.entity_id, "lock", {CONF_DEFAULT_CODE: "1234"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity0._lock_option_default_code == "1234"
|
||||||
|
|
||||||
|
|
||||||
async def test_lock_open_with_code(hass: HomeAssistant) -> None:
|
async def test_lock_open_with_code(hass: HomeAssistant) -> None:
|
||||||
"""Test lock entity with open service."""
|
"""Test lock entity with open service."""
|
||||||
lock = MockLockEntity(
|
lock = MockLockEntity(
|
||||||
@ -150,3 +231,20 @@ async def test_lock_unlock_with_code(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"}))
|
await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"}))
|
||||||
assert not lock.is_locked
|
assert not lock.is_locked
|
||||||
|
|
||||||
|
|
||||||
|
async def test_lock_with_default_code(hass: HomeAssistant) -> None:
|
||||||
|
"""Test lock entity with default code."""
|
||||||
|
lock = MockLockEntity(
|
||||||
|
code_format=r"^\d{4}$",
|
||||||
|
supported_features=LockEntityFeature.OPEN,
|
||||||
|
lock_option_default_code="1234",
|
||||||
|
)
|
||||||
|
lock.hass = hass
|
||||||
|
|
||||||
|
assert lock.state_attributes == {"code_format": r"^\d{4}$"}
|
||||||
|
assert lock._lock_option_default_code == "1234"
|
||||||
|
|
||||||
|
await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {}))
|
||||||
|
await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {}))
|
||||||
|
await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {}))
|
||||||
|
@ -43,6 +43,11 @@ async def async_setup_platform(
|
|||||||
class MockLock(MockEntity, LockEntity):
|
class MockLock(MockEntity, LockEntity):
|
||||||
"""Mock Lock class."""
|
"""Mock Lock class."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self) -> str | None:
|
||||||
|
"""Return code format."""
|
||||||
|
return self._handle("code_format")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
"""Return true if the lock is locked."""
|
"""Return true if the lock is locked."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user