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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user