mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
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:
parent
c92a90e04d
commit
c132900b92
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user