diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 8cbce69dc7c..ed7e2070055 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -87,40 +87,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None: - """Lock the lock.""" - code: str = service_call.data.get( - ATTR_CODE, entity._lock_option_default_code # pylint: disable=protected-access - ) +@callback +def _add_default_code(entity: LockEntity, service_call: ServiceCall) -> dict[Any, Any]: + data = remove_entity_service_fields(service_call) + code: str = data.pop(ATTR_CODE, "") + if not code: + code = entity._lock_option_default_code # pylint: disable=protected-access if entity.code_format_cmp and not entity.code_format_cmp.match(code): raise ValueError( f"Code '{code}' for locking {entity.entity_id} doesn't match pattern {entity.code_format}" ) - await entity.async_lock(**remove_entity_service_fields(service_call)) + if code: + data[ATTR_CODE] = code + return data + + +async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None: + """Lock the lock.""" + await entity.async_lock(**_add_default_code(entity, service_call)) async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None: """Unlock the lock.""" - 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): - raise ValueError( - f"Code '{code}' for unlocking {entity.entity_id} doesn't match pattern {entity.code_format}" - ) - await entity.async_unlock(**remove_entity_service_fields(service_call)) + await entity.async_unlock(**_add_default_code(entity, service_call)) async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None: """Open the door latch.""" - 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): - raise ValueError( - f"Code '{code}' for opening {entity.entity_id} doesn't match pattern {entity.code_format}" - ) - await entity.async_open(**remove_entity_service_fields(service_call)) + await entity.async_open(**_add_default_code(entity, service_call)) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 31ad8fc60ac..16f40fda786 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -42,6 +42,8 @@ class MockLockEntity(LockEntity): ) -> None: """Initialize mock lock entity.""" self._attr_supported_features = supported_features + self.calls_lock = MagicMock() + self.calls_unlock = MagicMock() self.calls_open = MagicMock() if code_format is not None: self._attr_code_format = code_format @@ -49,11 +51,13 @@ class MockLockEntity(LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" + self.calls_lock(kwargs) self._attr_is_locking = False self._attr_is_locked = True async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" + self.calls_unlock(kwargs) self._attr_is_unlocking = False self._attr_is_locked = False @@ -232,6 +236,50 @@ async def test_lock_unlock_with_code(hass: HomeAssistant) -> None: assert not lock.is_locked +async def test_lock_with_illegal_code(hass: HomeAssistant) -> None: + """Test lock entity with default code that does not match the code format.""" + lock = MockLockEntity( + code_format=r"^\d{4}$", + supported_features=LockEntityFeature.OPEN, + ) + lock.hass = hass + + with pytest.raises(ValueError): + await _async_open( + lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "123456"}) + ) + with pytest.raises(ValueError): + await _async_lock( + lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "123456"}) + ) + with pytest.raises(ValueError): + await _async_unlock( + lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "123456"}) + ) + + +async def test_lock_with_no_code(hass: HomeAssistant) -> None: + """Test lock entity with default code that does not match the code format.""" + lock = MockLockEntity( + supported_features=LockEntityFeature.OPEN, + ) + lock.hass = hass + + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {})) + lock.calls_open.assert_called_with({}) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + lock.calls_lock.assert_called_with({}) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + lock.calls_unlock.assert_called_with({}) + + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: ""})) + lock.calls_open.assert_called_with({}) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: ""})) + lock.calls_lock.assert_called_with({}) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: ""})) + lock.calls_unlock.assert_called_with({}) + + async def test_lock_with_default_code(hass: HomeAssistant) -> None: """Test lock entity with default code.""" lock = MockLockEntity( @@ -245,5 +293,52 @@ async def test_lock_with_default_code(hass: HomeAssistant) -> None: assert lock._lock_option_default_code == "1234" await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {})) + lock.calls_open.assert_called_with({ATTR_CODE: "1234"}) await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + lock.calls_lock.assert_called_with({ATTR_CODE: "1234"}) await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + lock.calls_unlock.assert_called_with({ATTR_CODE: "1234"}) + + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: ""})) + lock.calls_open.assert_called_with({ATTR_CODE: "1234"}) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: ""})) + lock.calls_lock.assert_called_with({ATTR_CODE: "1234"}) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: ""})) + lock.calls_unlock.assert_called_with({ATTR_CODE: "1234"}) + + +async def test_lock_with_provided_and_default_code(hass: HomeAssistant) -> None: + """Test lock entity with provided code when default code is set.""" + lock = MockLockEntity( + code_format=r"^\d{4}$", + supported_features=LockEntityFeature.OPEN, + lock_option_default_code="1234", + ) + lock.hass = hass + + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "4321"})) + lock.calls_open.assert_called_with({ATTR_CODE: "4321"}) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "4321"})) + lock.calls_lock.assert_called_with({ATTR_CODE: "4321"}) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "4321"})) + lock.calls_unlock.assert_called_with({ATTR_CODE: "4321"}) + + +async def test_lock_with_illegal_default_code(hass: HomeAssistant) -> None: + """Test lock entity with default code that does not match the code format.""" + lock = MockLockEntity( + code_format=r"^\d{4}$", + supported_features=LockEntityFeature.OPEN, + lock_option_default_code="123456", + ) + lock.hass = hass + + assert lock.state_attributes == {"code_format": r"^\d{4}$"} + assert lock._lock_option_default_code == "123456" + + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {})) + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + with pytest.raises(ValueError): + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {}))