Add zwave_js.set_lock_configuration service (#103595)

* Add zwave_js.set_lock_configuration service

* Add tests

* string tweaks

* Update homeassistant/components/zwave_js/lock.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update strings.json

* Update services.yaml

* Update lock.py

* Remove handle params

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2023-11-15 11:01:20 -05:00 committed by GitHub
parent c92a90e04d
commit c132900b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 275 additions and 7 deletions

View File

@ -99,6 +99,7 @@ SERVICE_REFRESH_VALUE = "refresh_value"
SERVICE_RESET_METER = "reset_meter"
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
SERVICE_SET_LOCK_CONFIGURATION = "set_lock_configuration"
SERVICE_SET_VALUE = "set_value"
ATTR_NODES = "nodes"
@ -118,6 +119,13 @@ ATTR_METER_TYPE_NAME = "meter_type_name"
# invoke CC API
ATTR_METHOD_NAME = "method_name"
ATTR_PARAMETERS = "parameters"
# lock set configuration
ATTR_AUTO_RELOCK_TIME = "auto_relock_time"
ATTR_BLOCK_TO_BLOCK = "block_to_block"
ATTR_HOLD_AND_RELEASE_TIME = "hold_and_release_time"
ATTR_LOCK_TIMEOUT = "lock_timeout"
ATTR_OPERATION_TYPE = "operation_type"
ATTR_TWIST_ASSIST = "twist_assist"
ADDON_SLUG = "core_zwave_js"

View File

@ -11,10 +11,12 @@ from zwave_js_server.const.command_class.lock import (
ATTR_USERCODE,
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP,
LOCK_CMD_CLASS_TO_PROPERTY_MAP,
DoorLockCCConfigurationSetOptions,
DoorLockMode,
OperationType,
)
from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.util.lock import clear_usercode, set_usercode
from zwave_js_server.util.lock import clear_usercode, set_configuration, set_usercode
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
from homeassistant.config_entries import ConfigEntry
@ -26,10 +28,17 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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,
DATA_CLIENT,
DOMAIN,
LOGGER,
SERVICE_CLEAR_LOCK_USERCODE,
SERVICE_SET_LOCK_CONFIGURATION,
SERVICE_SET_LOCK_USERCODE,
)
from .discovery import ZwaveDiscoveryInfo
@ -47,6 +56,7 @@ STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = {
STATE_LOCKED: True,
},
}
UNIT16_SCHEMA = vol.All(vol.Coerce(int), vol.Range(min=0, max=65535))
async def async_setup_entry(
@ -92,6 +102,24 @@ async def async_setup_entry(
"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."""
@ -138,9 +166,10 @@ 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 code_slot {code_slot}: {err}"
f"Unable to set lock usercode on lock {self.entity_id} code_slot "
f"{code_slot}: {err}"
) from err
LOGGER.debug("User code at slot %s set", code_slot)
LOGGER.debug("User code at slot %s on lock %s set", code_slot, self.entity_id)
async def async_clear_lock_usercode(self, code_slot: int) -> None:
"""Clear the usercode at index X on the lock."""
@ -148,6 +177,41 @@ 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 code_slot {code_slot}: {err}"
f"Unable to clear lock usercode on lock {self.entity_id} code_slot "
f"{code_slot}: {err}"
) from err
LOGGER.debug("User code at slot %s cleared", code_slot)
LOGGER.debug(
"User code at slot %s on lock %s cleared", code_slot, self.entity_id
)
async def async_set_lock_configuration(
self,
operation_type: OperationType,
lock_timeout: int | None = None,
auto_relock_time: int | None = None,
hold_and_release_time: int | None = None,
twist_assist: bool | None = None,
block_to_block: bool | None = None,
) -> None:
"""Set the lock configuration."""
params: dict[str, Any] = {"operation_type": operation_type}
for attr, val in (
("lock_timeout_configuration", lock_timeout),
("auto_relock_time", auto_relock_time),
("hold_and_release_time", hold_and_release_time),
("twist_assist", twist_assist),
("block_to_block", block_to_block),
):
if val is not None:
params[attr] = val
configuration = DoorLockCCConfigurationSetOptions(**params)
result = await set_configuration(
self.info.node.endpoints[self.info.primary_value.endpoint or 0],
configuration,
)
if result is None:
return
msg = f"Result status is {result.status}"
if result.remaining_duration is not None:
msg += f" and remaining duration is {str(result.remaining_duration)}"
LOGGER.info("%s after setting lock configuration for %s", msg, self.entity_id)

View File

@ -29,6 +29,65 @@ set_lock_usercode:
selector:
text:
set_lock_configuration:
target:
entity:
domain: lock
integration: zwave_js
fields:
operation_type:
required: true
example: timed
selector:
select:
options:
- constant
- timed
lock_timeout:
required: false
example: 1
selector:
number:
min: 0
max: 65535
unit_of_measurement: sec
outside_handles_can_open_door_configuration:
required: false
example: [true, true, true, false]
selector:
object:
inside_handles_can_open_door_configuration:
required: false
example: [true, true, true, false]
selector:
object:
auto_relock_time:
required: false
example: 1
selector:
number:
min: 0
max: 65535
unit_of_measurement: sec
hold_and_release_time:
required: false
example: 1
selector:
number:
min: 0
max: 65535
unit_of_measurement: sec
twist_assist:
required: false
example: true
selector:
boolean:
block_to_block:
required: false
example: true
selector:
boolean:
set_config_parameter:
target:
entity:

View File

@ -385,6 +385,44 @@
"description": "The Notification Event number as defined in the Z-Wave specs."
}
}
},
"set_lock_configuration": {
"name": "Set lock configuration",
"description": "Sets the configuration for a lock.",
"fields": {
"operation_type": {
"name": "Operation Type",
"description": "The operation type of the lock."
},
"lock_timeout": {
"name": "Lock timeout",
"description": "Seconds until lock mode times out. Should only be used if operation type is `timed`."
},
"outside_handles_can_open_door_configuration": {
"name": "Outside handles can open door configuration",
"description": "A list of four booleans which indicate which outside handles can open the door."
},
"inside_handles_can_open_door_configuration": {
"name": "Inside handles can open door configuration",
"description": "A list of four booleans which indicate which inside handles can open the door."
},
"auto_relock_time": {
"name": "Auto relock time",
"description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`."
},
"hold_and_release_time": {
"name": "Hold and release time",
"description": "Duration in seconds the latch stays retracted."
},
"twist_assist": {
"name": "Twist assist",
"description": "Enable Twist Assist."
},
"block_to_block": {
"name": "Block to block",
"description": "Enable block-to-block functionality."
}
}
}
}
}

View File

@ -15,10 +15,15 @@ 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.const import (
ATTR_LOCK_TIMEOUT,
ATTR_OPERATION_TYPE,
DOMAIN as ZWAVE_JS_DOMAIN,
)
from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
from homeassistant.components.zwave_js.lock import (
SERVICE_CLEAR_LOCK_USERCODE,
SERVICE_SET_LOCK_CONFIGURATION,
SERVICE_SET_LOCK_USERCODE,
)
from homeassistant.const import (
@ -35,7 +40,11 @@ from .common import SCHLAGE_BE469_LOCK_ENTITY, replace_value_of_zwave_value
async def test_door_lock(
hass: HomeAssistant, client, lock_schlage_be469, integration
hass: HomeAssistant,
client,
lock_schlage_be469,
integration,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test a lock entity with door lock command class."""
node = lock_schlage_be469
@ -158,6 +167,96 @@ async def test_door_lock(
client.async_send_command.reset_mock()
# Test set configuration
client.async_send_command.return_value = {
"response": {"status": 1, "remainingDuration": "default"}
}
caplog.clear()
await hass.services.async_call(
ZWAVE_JS_DOMAIN,
SERVICE_SET_LOCK_CONFIGURATION,
{
ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY,
ATTR_OPERATION_TYPE: "timed",
ATTR_LOCK_TIMEOUT: 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"] == "endpoint.invoke_cc_api"
assert args["nodeId"] == 20
assert args["endpoint"] == 0
assert args["args"] == [
{
"insideHandlesCanOpenDoorConfiguration": [True, True, True, True],
"operationType": 2,
"outsideHandlesCanOpenDoorConfiguration": [True, True, True, True],
}
]
assert args["commandClass"] == 98
assert args["methodName"] == "setConfiguration"
assert "Result status" in caplog.text
assert "remaining duration" in caplog.text
assert "setting lock configuration" in caplog.text
client.async_send_command.reset_mock()
client.async_send_command_no_wait.reset_mock()
caplog.clear()
# Put node to sleep and validate that we don't wait for a return or log anything
event = Event(
"sleep",
{
"source": "node",
"event": "sleep",
"nodeId": node.node_id,
},
)
node.receive_event(event)
await hass.services.async_call(
ZWAVE_JS_DOMAIN,
SERVICE_SET_LOCK_CONFIGURATION,
{
ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY,
ATTR_OPERATION_TYPE: "timed",
ATTR_LOCK_TIMEOUT: 1,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 0
assert len(client.async_send_command_no_wait.call_args_list) == 1
args = client.async_send_command_no_wait.call_args[0][0]
assert args["command"] == "endpoint.invoke_cc_api"
assert args["nodeId"] == 20
assert args["endpoint"] == 0
assert args["args"] == [
{
"insideHandlesCanOpenDoorConfiguration": [True, True, True, True],
"operationType": 2,
"outsideHandlesCanOpenDoorConfiguration": [True, True, True, True],
}
]
assert args["commandClass"] == 98
assert args["methodName"] == "setConfiguration"
assert "Result status" not in caplog.text
assert "remaining duration" not in caplog.text
assert "setting lock configuration" not in caplog.text
# Mark node as alive
event = Event(
"alive",
{
"source": "node",
"event": "alive",
"nodeId": node.node_id,
},
)
node.receive_event(event)
client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test")
# Test set usercode service error handling
with pytest.raises(HomeAssistantError):