1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-14 23:28:42 +00:00

Z-Wave lock service action modernization (#162967)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Anders Ödlund
2026-02-14 01:33:23 -08:00
committed by GitHub
parent 7c8b181e6d
commit 2ef7f6b317
4 changed files with 125 additions and 67 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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.",

View File

@@ -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,