diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 088af87457e..1bdb70bcaa3 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -4,11 +4,8 @@ from __future__ import annotations from typing import Any -import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import ( - ATTR_CODE_SLOT, - ATTR_USERCODE, LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP, LOCK_CMD_CLASS_TO_PROPERTY_MAP, DoorLockCCConfigurationSetOptions, @@ -27,23 +24,10 @@ from zwave_js_server.util.lock import ( from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity, LockState from homeassistant.core import HomeAssistant, ServiceResponse, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - ATTR_AUTO_RELOCK_TIME, - ATTR_BLOCK_TO_BLOCK, - ATTR_HOLD_AND_RELEASE_TIME, - ATTR_LOCK_TIMEOUT, - ATTR_OPERATION_TYPE, - ATTR_TWIST_ASSIST, - DOMAIN, - LOGGER, - SERVICE_CLEAR_LOCK_USERCODE, - SERVICE_SET_LOCK_CONFIGURATION, - SERVICE_SET_LOCK_USERCODE, -) +from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .models import ZwaveJSConfigEntry @@ -60,7 +44,6 @@ STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = { LockState.LOCKED: True, }, } -UNIT16_SCHEMA = vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)) async def async_setup_entry( @@ -87,43 +70,6 @@ async def async_setup_entry( ) ) - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_SET_LOCK_USERCODE, - { - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - }, - "async_set_lock_usercode", - ) - - platform.async_register_entity_service( - SERVICE_CLEAR_LOCK_USERCODE, - { - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - }, - "async_clear_lock_usercode", - ) - - platform.async_register_entity_service( - SERVICE_SET_LOCK_CONFIGURATION, - { - vol.Required(ATTR_OPERATION_TYPE): vol.All( - cv.string, - vol.Upper, - vol.In(["TIMED", "CONSTANT"]), - lambda x: OperationType[x], - ), - vol.Optional(ATTR_LOCK_TIMEOUT): UNIT16_SCHEMA, - vol.Optional(ATTR_AUTO_RELOCK_TIME): UNIT16_SCHEMA, - vol.Optional(ATTR_HOLD_AND_RELEASE_TIME): UNIT16_SCHEMA, - vol.Optional(ATTR_TWIST_ASSIST): vol.Coerce(bool), - vol.Optional(ATTR_BLOCK_TO_BLOCK): vol.Coerce(bool), - }, - "async_set_lock_configuration", - ) - class ZWaveLock(ZWaveBaseEntity, LockEntity): """Representation of a Z-Wave lock.""" @@ -170,8 +116,13 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): await set_usercode(self.info.node, code_slot, usercode) except BaseZwaveJSServerError as err: raise HomeAssistantError( - f"Unable to set lock usercode on lock {self.entity_id} code_slot " - f"{code_slot}: {err}" + translation_domain=DOMAIN, + translation_key="set_lock_usercode_failed", + translation_placeholders={ + "entity_id": self.entity_id, + "code_slot": str(code_slot), + "error": str(err), + }, ) from err LOGGER.debug("User code at slot %s on lock %s set", code_slot, self.entity_id) @@ -222,8 +173,13 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): await clear_usercode(self.info.node, code_slot) except BaseZwaveJSServerError as err: raise HomeAssistantError( - f"Unable to clear lock usercode on lock {self.entity_id} code_slot " - f"{code_slot}: {err}" + translation_domain=DOMAIN, + translation_key="clear_lock_usercode_failed", + translation_placeholders={ + "entity_id": self.entity_id, + "code_slot": str(code_slot), + "error": str(err), + }, ) from err LOGGER.debug( "User code at slot %s on lock %s cleared", code_slot, self.entity_id diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 34c96d4469c..524a5b6c548 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -11,7 +11,11 @@ from typing import Any import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus -from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT +from zwave_js_server.const.command_class.lock import ( + ATTR_CODE_SLOT, + ATTR_USERCODE, + OperationType, +) from zwave_js_server.const.command_class.notification import NotificationType from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed from zwave_js_server.model.endpoint import Endpoint @@ -54,6 +58,8 @@ _LOGGER = logging.getLogger(__name__) type _NodeOrEndpointType = ZwaveNode | Endpoint +UNIT16_SCHEMA = vol.All(vol.Coerce(int), vol.Range(min=0, max=65535)) + TARGET_VALIDATORS = { vol.Optional(ATTR_AREA_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), @@ -494,6 +500,50 @@ class ZWaveServices: supports_response=SupportsResponse.ONLY, ) + async_register_platform_entity_service( + self._hass, + const.DOMAIN, + const.SERVICE_SET_LOCK_USERCODE, + entity_domain=LOCK_DOMAIN, + schema={ + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + vol.Required(ATTR_USERCODE): cv.string, + }, + func="async_set_lock_usercode", + ) + + async_register_platform_entity_service( + self._hass, + const.DOMAIN, + const.SERVICE_CLEAR_LOCK_USERCODE, + entity_domain=LOCK_DOMAIN, + schema={ + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + }, + func="async_clear_lock_usercode", + ) + + async_register_platform_entity_service( + self._hass, + const.DOMAIN, + const.SERVICE_SET_LOCK_CONFIGURATION, + entity_domain=LOCK_DOMAIN, + schema={ + vol.Required(const.ATTR_OPERATION_TYPE): vol.All( + cv.string, + vol.Upper, + vol.In(["TIMED", "CONSTANT"]), + lambda x: OperationType[x], + ), + vol.Optional(const.ATTR_LOCK_TIMEOUT): UNIT16_SCHEMA, + vol.Optional(const.ATTR_AUTO_RELOCK_TIME): UNIT16_SCHEMA, + vol.Optional(const.ATTR_HOLD_AND_RELEASE_TIME): UNIT16_SCHEMA, + vol.Optional(const.ATTR_TWIST_ASSIST): vol.Coerce(bool), + vol.Optional(const.ATTR_BLOCK_TO_BLOCK): vol.Coerce(bool), + }, + func="async_set_lock_configuration", + ) + async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index cc62aeab4c0..143c43c422c 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -292,8 +292,14 @@ } }, "exceptions": { + "clear_lock_usercode_failed": { + "message": "Unable to clear lock usercode on lock {entity_id} code_slot {code_slot}: {error}" + }, "get_lock_usercode_not_found": { "message": "Code slot {code_slot} not found on lock {entity_id}" + }, + "set_lock_usercode_failed": { + "message": "Unable to set lock usercode on lock {entity_id} code_slot {code_slot}: {error}" } }, "issues": { @@ -376,7 +382,7 @@ "fields": { "code_slot": { "description": "Code slot to clear code from.", - "name": "Code slot" + "name": "[%key:component::zwave_js::device_automation::extra_fields::code_slot%]" } }, "name": "Clear lock user code" @@ -386,7 +392,7 @@ "fields": { "code_slot": { "description": "Code slot to get code from. If not specified, all code slots are returned.", - "name": "Code slot" + "name": "[%key:component::zwave_js::device_automation::extra_fields::code_slot%]" } }, "name": "Get lock user code" @@ -632,7 +638,7 @@ "fields": { "code_slot": { "description": "Code slot to set the code.", - "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]" + "name": "[%key:component::zwave_js::device_automation::extra_fields::code_slot%]" }, "usercode": { "description": "Lock code to set.", diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 6723a757670..5908f9d9116 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -23,14 +23,12 @@ from homeassistant.components.zwave_js.const import ( ATTR_LOCK_TIMEOUT, ATTR_OPERATION_TYPE, DOMAIN, - SERVICE_GET_LOCK_USERCODE, -) -from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher -from homeassistant.components.zwave_js.lock import ( SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_GET_LOCK_USERCODE, SERVICE_SET_LOCK_CONFIGURATION, SERVICE_SET_LOCK_USERCODE, ) +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -417,6 +415,54 @@ async def test_get_lock_usercode( } +async def test_set_lock_usercode_error( + hass: HomeAssistant, + client, + lock_schlage_be469, + integration, +) -> None: + """Test set lock usercode service error handling.""" + client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_LOCK_USERCODE, + { + ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, + ATTR_CODE_SLOT: 1, + ATTR_USERCODE: "1234", + }, + blocking=True, + ) + + assert str(exc_info.value) == ( + "Unable to set lock usercode on lock " + f"{SCHLAGE_BE469_LOCK_ENTITY} code_slot 1: zwave_error: Z-Wave error 1 - test" + ) + + +async def test_clear_lock_usercode_error( + hass: HomeAssistant, + client, + lock_schlage_be469, + integration, +) -> None: + """Test clear lock usercode service error handling.""" + client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_LOCK_USERCODE, + {ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, ATTR_CODE_SLOT: 1}, + blocking=True, + ) + + assert str(exc_info.value) == ( + "Unable to clear lock usercode on lock " + f"{SCHLAGE_BE469_LOCK_ENTITY} code_slot 1: zwave_error: Z-Wave error 1 - test" + ) + + async def test_get_lock_usercode_error( hass: HomeAssistant, client,