diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 830cf4d6564..dedaf9a5e45 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -2,19 +2,24 @@ import logging from typing import Any, Callable, Dict, List, Optional, Union +import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( + ATTR_CODE_SLOT, + ATTR_USERCODE, LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP, LOCK_CMD_CLASS_TO_PROPERTY_MAP, CommandClass, DoorLockMode, ) from zwave_js_server.model.value import Value as ZwaveValue +from zwave_js_server.util.lock import clear_usercode, set_usercode from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN @@ -34,6 +39,9 @@ STATE_TO_ZWAVE_MAP: Dict[int, Dict[str, Union[int, bool]]] = { }, } +SERVICE_SET_LOCK_USERCODE = "set_lock_usercode" +SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode" + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable @@ -55,6 +63,26 @@ async def async_setup_entry( ) ) + platform = entity_platform.current_platform.get() + assert platform + + platform.async_register_entity_service( # type: ignore + 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( # type: ignore + SERVICE_CLEAR_LOCK_USERCODE, + { + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + }, + "async_clear_lock_usercode", + ) + class ZWaveLock(ZWaveBaseEntity, LockEntity): """Representation of a Z-Wave lock.""" @@ -88,3 +116,13 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): async def async_unlock(self, **kwargs: Dict[str, Any]) -> None: """Unlock the lock.""" await self._set_lock_state(STATE_UNLOCKED) + + async def async_set_lock_usercode(self, code_slot: int, usercode: str) -> None: + """Set the usercode to index X on the lock.""" + await set_usercode(self.info.node, code_slot, usercode) + LOGGER.debug("User code at slot %s set", code_slot) + + async def async_clear_lock_usercode(self, code_slot: int) -> None: + """Clear the usercode at index X on the lock.""" + await clear_usercode(self.info.node, code_slot) + LOGGER.debug("User code at slot %s cleared", code_slot) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml new file mode 100644 index 00000000000..cc81da7ed58 --- /dev/null +++ b/homeassistant/components/zwave_js/services.yaml @@ -0,0 +1,24 @@ +# Describes the format for available Z-Wave services + +clear_lock_usercode: + description: Clear a usercode from lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to clear code from. + example: 1 + +set_lock_usercode: + description: Set a usercode to lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to set the code. + example: 1 + usercode: + description: Code to set. + example: 1234 diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index bf2d3f3c5c2..069b3497a55 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS lock platform.""" +from zwave_js_server.const import ATTR_CODE_SLOT, ATTR_USERCODE from zwave_js_server.event import Event from homeassistant.components.lock import ( @@ -6,6 +7,11 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) +from homeassistant.components.zwave_js.const import DOMAIN as ZWAVE_JS_DOMAIN +from homeassistant.components.zwave_js.lock import ( + SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_SET_LOCK_USERCODE, +) from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt_current_lock_mode" @@ -122,3 +128,78 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): }, } assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test set usercode service + await hass.services.async_call( + ZWAVE_JS_DOMAIN, + SERVICE_SET_LOCK_USERCODE, + { + ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, + ATTR_CODE_SLOT: 1, + ATTR_USERCODE: "1234", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyName": "userCode", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "string", + "readable": True, + "writeable": True, + "minLength": 4, + "maxLength": 10, + "label": "User Code (1)", + }, + "value": "**********", + } + assert args["value"] == "1234" + + client.async_send_command.reset_mock() + + # Test clear usercode + await hass.services.async_call( + ZWAVE_JS_DOMAIN, + SERVICE_CLEAR_LOCK_USERCODE, + {ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, ATTR_CODE_SLOT: 1}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyName": "userIdStatus", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + }, + }, + "value": 1, + } + assert args["value"] == 0