mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +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_RESET_METER = "reset_meter"
|
||||||
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
|
||||||
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
||||||
|
SERVICE_SET_LOCK_CONFIGURATION = "set_lock_configuration"
|
||||||
SERVICE_SET_VALUE = "set_value"
|
SERVICE_SET_VALUE = "set_value"
|
||||||
|
|
||||||
ATTR_NODES = "nodes"
|
ATTR_NODES = "nodes"
|
||||||
@ -118,6 +119,13 @@ ATTR_METER_TYPE_NAME = "meter_type_name"
|
|||||||
# invoke CC API
|
# invoke CC API
|
||||||
ATTR_METHOD_NAME = "method_name"
|
ATTR_METHOD_NAME = "method_name"
|
||||||
ATTR_PARAMETERS = "parameters"
|
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"
|
ADDON_SLUG = "core_zwave_js"
|
||||||
|
|
||||||
|
@ -11,10 +11,12 @@ from zwave_js_server.const.command_class.lock import (
|
|||||||
ATTR_USERCODE,
|
ATTR_USERCODE,
|
||||||
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP,
|
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP,
|
||||||
LOCK_CMD_CLASS_TO_PROPERTY_MAP,
|
LOCK_CMD_CLASS_TO_PROPERTY_MAP,
|
||||||
|
DoorLockCCConfigurationSetOptions,
|
||||||
DoorLockMode,
|
DoorLockMode,
|
||||||
|
OperationType,
|
||||||
)
|
)
|
||||||
from zwave_js_server.exceptions import BaseZwaveJSServerError
|
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.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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 homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
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,
|
DATA_CLIENT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
SERVICE_CLEAR_LOCK_USERCODE,
|
SERVICE_CLEAR_LOCK_USERCODE,
|
||||||
|
SERVICE_SET_LOCK_CONFIGURATION,
|
||||||
SERVICE_SET_LOCK_USERCODE,
|
SERVICE_SET_LOCK_USERCODE,
|
||||||
)
|
)
|
||||||
from .discovery import ZwaveDiscoveryInfo
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
@ -47,6 +56,7 @@ STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = {
|
|||||||
STATE_LOCKED: True,
|
STATE_LOCKED: True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
UNIT16_SCHEMA = vol.All(vol.Coerce(int), vol.Range(min=0, max=65535))
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -92,6 +102,24 @@ async def async_setup_entry(
|
|||||||
"async_clear_lock_usercode",
|
"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):
|
class ZWaveLock(ZWaveBaseEntity, LockEntity):
|
||||||
"""Representation of a Z-Wave lock."""
|
"""Representation of a Z-Wave lock."""
|
||||||
@ -138,9 +166,10 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
|
|||||||
await set_usercode(self.info.node, code_slot, usercode)
|
await set_usercode(self.info.node, code_slot, usercode)
|
||||||
except BaseZwaveJSServerError as err:
|
except BaseZwaveJSServerError as err:
|
||||||
raise HomeAssistantError(
|
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
|
) 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:
|
async def async_clear_lock_usercode(self, code_slot: int) -> None:
|
||||||
"""Clear the usercode at index X on the lock."""
|
"""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)
|
await clear_usercode(self.info.node, code_slot)
|
||||||
except BaseZwaveJSServerError as err:
|
except BaseZwaveJSServerError as err:
|
||||||
raise HomeAssistantError(
|
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
|
) 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:
|
selector:
|
||||||
text:
|
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:
|
set_config_parameter:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
@ -385,6 +385,44 @@
|
|||||||
"description": "The Notification Event number as defined in the Z-Wave specs."
|
"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_LOCK,
|
||||||
SERVICE_UNLOCK,
|
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.helpers import ZwaveValueMatcher
|
||||||
from homeassistant.components.zwave_js.lock import (
|
from homeassistant.components.zwave_js.lock import (
|
||||||
SERVICE_CLEAR_LOCK_USERCODE,
|
SERVICE_CLEAR_LOCK_USERCODE,
|
||||||
|
SERVICE_SET_LOCK_CONFIGURATION,
|
||||||
SERVICE_SET_LOCK_USERCODE,
|
SERVICE_SET_LOCK_USERCODE,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
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(
|
async def test_door_lock(
|
||||||
hass: HomeAssistant, client, lock_schlage_be469, integration
|
hass: HomeAssistant,
|
||||||
|
client,
|
||||||
|
lock_schlage_be469,
|
||||||
|
integration,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test a lock entity with door lock command class."""
|
"""Test a lock entity with door lock command class."""
|
||||||
node = lock_schlage_be469
|
node = lock_schlage_be469
|
||||||
@ -158,6 +167,96 @@ async def test_door_lock(
|
|||||||
|
|
||||||
client.async_send_command.reset_mock()
|
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")
|
client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test")
|
||||||
# Test set usercode service error handling
|
# Test set usercode service error handling
|
||||||
with pytest.raises(HomeAssistantError):
|
with pytest.raises(HomeAssistantError):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user