diff --git a/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua b/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua index 13d45bc4dd..7ebd38d191 100644 --- a/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua @@ -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 @@ -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 @@ -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) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua index 5d48a4d576..a4bc87bfe0 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua @@ -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" }, @@ -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 } })) @@ -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 @@ -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() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua index 21c829c497..6198015fde 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua @@ -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" }, @@ -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 } }))