Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ function CapabilityHandlers.add_credential(driver, device, command)
end

function CapabilityHandlers.update_credential(driver, device, command)
-- Index 0 is the Z-Wave "all users" wildcard and is never a valid credential to update.
if command.args.credentialIndex == 0 then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

if lock_utils.is_device_busy(device) then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.BUSY)
return
Expand All @@ -165,6 +171,12 @@ function CapabilityHandlers.update_credential(driver, device, command)
end

function CapabilityHandlers.delete_credential(driver, device, command)
-- Index 0 is the Z-Wave "all users" wildcard; clearing everything must go through deleteAllCredentials.
if command.args.credentialIndex == 0 then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS)

if cmd_in_progress == consts.LOCK_USERS.DELETE then
Expand Down Expand Up @@ -199,19 +211,13 @@ function CapabilityHandlers.delete_all_credentials(driver, device, command)
local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS)

local send_clear_commands = function()
local credentials = tables.get_state(device, "credentials") or {}
local delay = 0
for _, credential in ipairs(credentials) do
device.thread:call_with_delay(delay, function()
device:send(UserCode:Set({
user_identifier = credential.credentialIndex,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
end)
delay = delay + 2
end
-- Delayed cleanup: ensure all tables are cleared after individual responses complete
device.thread:call_with_delay(delay + 4, function()
-- User Identifier 0 with status AVAILABLE tells the lock to clear every user code slot
device:send(UserCode:Set({
user_identifier = 0,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
-- Delayed cleanup: ensure all tables are cleared after the lock has processed the command
device.thread:call_with_delay(4, function()
if cmd_in_progress == consts.LOCK_USERS.DELETE_ALL then
tables.delete_all_entries(device, "users")
lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, consts.COMMAND_RESULT.SUCCESS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ test.register_coroutine_test(
-- ============================================================================

test.register_coroutine_test(
"deleteAllCredentials: sends ClearAllPINCodes and emits success when the lock returns PASS",
"deleteAllCredentials: sends a single UserCode Set for identifier 0 and emits success",
function()
seed_users({
{ userIndex = 1, userName = "Alice", userType = "guest" },
Expand All @@ -519,14 +519,12 @@ test.register_coroutine_test(
{ capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} },
})

-- Per-credential clears fire at 2 * (i - 1) s; final cleanup at 2 * #credentials + 4 s.
test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot")
expect_clear_pin_code(1)
test.mock_time.advance_time(0)
-- A single wildcard clear (identifier 0) is sent immediately; final cleanup fires at 4 s.
test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot")
expect_clear_pin_code(0)
test.wait_for_events()

test.mock_time.advance_time(6)
test.mock_time.advance_time(4)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))
Expand Down Expand Up @@ -578,7 +576,8 @@ test.register_coroutine_test(
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} },
})
-- deleteAllCredentials sends individual UserCode:Set AVAILABLE commands
-- deleteAllCredentials sends a single UserCode:Set (identifier 0) AVAILABLE command
expect_clear_pin_code(0)
test.wait_for_events()

-- Second deleteAllCredentials while first is pending → busy
Expand All @@ -600,4 +599,48 @@ test.register_coroutine_test(
}
)

test.register_coroutine_test(
"deleteCredential: rejects credentialIndex 0 with invalidCommand and sends no z-wave command",
function()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "deleteCredential",
args = { 0, "pin" } },
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "deleteCredential", statusCode = "invalidCommand" },
{ state_change = true, visibility = { displayed = false } }
))
)
test.wait_for_events()
end,
{
min_api_version = 15
}
)

test.register_coroutine_test(
"updateCredential: rejects credentialIndex 0 with invalidCommand and sends no z-wave command",
function()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "updateCredential",
args = { 1, 0, "pin", "1234" } },
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "updateCredential", statusCode = "invalidCommand" },
{ state_change = true, visibility = { displayed = false } }
))
)
test.wait_for_events()
end,
{
min_api_version = 15
}
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ test.register_coroutine_test(
-- ============================================================================

test.register_coroutine_test(
"deleteAllUsers: sends ClearAllPINCodes and emits success for both users and credentials on PASS",
"deleteAllUsers: sends a single UserCode Set for identifier 0 and emits success for both users and credentials on PASS",
function()
seed_users({
{ userIndex = 1, userName = "Alice", userType = "guest" },
Expand All @@ -485,19 +485,13 @@ test.register_coroutine_test(
{ capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} },
})

test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(0.5, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(8, "oneshot")
test.mock_time.advance_time(0)
expect_clear_pin_code(1)
test.wait_for_events()
test.mock_time.advance_time(0.5)
expect_clear_pin_code(2)
test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot")
expect_clear_pin_code(0)
test.wait_for_events()

-- The legacy subdriver consumes deletion notifications for this mock profile; advance
-- to the driver's cleanup timer, which clears both tables and reports success.
test.mock_time.advance_time(8)
test.mock_time.advance_time(4)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockUsers.users({}, { visibility = { displayed = false } }))
Expand Down
Loading