From fc309502ae074b5c78e1e15d6e12005c824a6b4e Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 30 Apr 2026 12:55:08 -0500 Subject: [PATCH 1/7] CHAD-16364: Update Z-Wave lock capabilities --- .../src/using-new-capabilities/init.lua | 568 ++++++++++++++ .../src/using-old-capabilities/init.lua | 413 ++++++++++ .../zwave-lock/profiles/base-lock-tamper.yml | 4 + .../zwave-lock/profiles/base-lock.yml | 4 + drivers/SmartThings/zwave-lock/src/init.lua | 144 +--- .../zwave-lock/src/lazy_load_subdriver.lua | 2 - .../zwave-lock/src/new_lock_utils.lua | 489 ++++++++++++ .../zwave-lock/src/sub_drivers.lua | 6 +- .../zwave-lock/src/test/test_keywe_lock.lua | 8 +- .../test/test_keywe_lock_new_capabilities.lua | 128 +++ .../zwave-lock/src/test/test_lock_battery.lua | 6 +- .../zwave-lock/src/test/test_samsung_lock.lua | 6 +- .../test_samsung_lock_new_capabilities.lua | 263 +++++++ .../zwave-lock/src/test/test_schlage_lock.lua | 7 +- .../test_schlage_lock_new_capabilities.lua | 298 +++++++ .../zwave-lock/src/test/test_zwave_lock.lua | 14 +- .../test_zwave_lock_code_slga_migration.lua | 127 +++ .../test/test_zwave_lock_new_capabilities.lua | 730 ++++++++++++++++++ .../src/using-new-capabilities/can_handle.lua | 13 + .../src/using-new-capabilities/init.lua | 418 ++++++++++ .../keywe-lock/can_handle.lua | 11 + .../keywe-lock/init.lua | 69 ++ .../samsung-lock/can_handle.lua | 11 + .../samsung-lock/init.lua | 64 ++ .../schlage-lock/can_handle.lua | 11 + .../schlage-lock/init.lua | 117 +++ .../using-new-capabilities/sub_drivers.lua | 11 + .../zwave-alarm-v1-lock/can_handle.lua | 10 + .../zwave-alarm-v1-lock/init.lua | 175 +++++ .../src/using-old-capabilities/can_handle.lua | 13 + .../src/using-old-capabilities/init.lua | 109 +++ .../keywe-lock/can_handle.lua | 7 +- .../keywe-lock/init.lua | 5 +- .../samsung-lock/can_handle.lua | 7 +- .../samsung-lock/init.lua | 7 +- .../schlage-lock/can_handle.lua | 7 +- .../schlage-lock/init.lua | 5 +- .../using-old-capabilities/sub_drivers.lua | 11 + .../zwave-alarm-v1-lock/can_handle.lua | 7 +- .../zwave-alarm-v1-lock/init.lua | 12 +- 40 files changed, 4148 insertions(+), 169 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/new_lock_utils.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/keywe-lock/can_handle.lua (58%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/keywe-lock/init.lua (95%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/samsung-lock/can_handle.lua (57%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/samsung-lock/init.lua (96%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/schlage-lock/can_handle.lua (57%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/schlage-lock/init.lua (98%) create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/zwave-alarm-v1-lock/can_handle.lua (60%) rename drivers/SmartThings/zwave-lock/src/{ => using-old-capabilities}/zwave-alarm-v1-lock/init.lua (95%) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua new file mode 100644 index 0000000000..8fff4ee861 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,568 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Driver utilities +local device_management = require "st.zigbee.device_management" +local log = require "log" +local utils = require "st.utils" + + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "new_lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(device) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfTotalUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) + end + + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(15, function(d) + reload_all_codes(device) + end) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + device.thread:call_with_delay(2, function(d) + reload_all_codes(device) + end) +end + +local add_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then + return + end + local available_index = lock_utils.get_available_user_index(device) + local status = lock_utils.STATUS_SUCCESS + if available_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + else + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) + lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) + end + + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) + end + + lock_utils.clear_busy_state(device, status) +end + +local update_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then + return + end + + local user_name = command.args.userName + local user_type = command.args.userType + local user_index = tonumber(command.args.userIndex) + local current_users = lock_utils.get_users(device) + local status = lock_utils.STATUS_FAILURE + + for _, user in pairs(current_users) do + if user.userIndex == user_index then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index}) + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break + end + end + + lock_utils.clear_busy_state(device, status) +end + +local delete_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then + return + end + local status = lock_utils.STATUS_SUCCESS + local user_index = tonumber(command.args.userIndex) + if lock_utils.get_user(device, user_index) ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + end + + local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) + if associated_credential ~= nil then + -- if there is an associated credential with this user then delete the credential + -- this command also handles the user deletion + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { associated_credential.credentialIndex, "pin" }, + override_busy_check = true + }) + else + lock_utils.delete_user(device, user_index) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end + else + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end +end + +local delete_all_users_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then + return + end + local status = lock_utils.STATUS_SUCCESS + local current_users = lock_utils.get_users(device) + + local delay = 0 + for _, user in pairs(current_users) do + device.thread:call_with_delay(delay, function() + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = {user.userIndex}, + override_busy_check = true + }) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local add_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local user_index = tonumber(command.args.userIndex) + local user_type = command.args.userType + local credential_type = command.args.credentialType + local credential_data = command.args.credentialData + local status = lock_utils.STATUS_SUCCESS + + local credential_index = lock_utils.get_available_credential_index(device) + if credential_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then + status = lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then + status = lock_utils.STATUS_FAILURE + end + + if user_index == 0 then + user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, user_type, user_index) + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + + if status == lock_utils.STATUS_SUCCESS then + -- set the pin code and then validate it was successful when the GetPINCode response is received. + -- the credential creation and events will also be handled in that response. + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, status) + end +end + +local update_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credential_index = tonumber(command.args.credentialIndex) + local credential_data = command.args.credentialData + local credential = lock_utils.get_credential(device, credential_index) + + if credential ~= nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function() + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) + end +end + +local delete_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then + return + end + + local credential_index = tonumber(command.args.credentialIndex) + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + end + + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) + end +end + +local delete_all_credentials_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS + local delay = 0 + for _, credential in pairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + end) + device.thread:call_with_delay(delay + 2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local max_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local min_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local max_codes_handler = function(driver, device, value) + device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + local status = lock_utils.STATUS_SUCCESS + local emit_event = false + + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- create credential if not already present. + if lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_event = true + end + elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then + -- update credential + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_event = true + end + else + -- Called by reloading the codes. Don't add if already in table. + if lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = lock_utils.get_available_user_index(device) + if new_user_index ~= nil then + lock_utils.create_user(device, nil, "guest", new_user_index) + lock_utils.add_credential(device, + new_user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_event = true + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + end + elseif zb_mess.body.zcl_body.user_status.value == UserStatusEnum.AVAILABLE and command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- tried to add a code that already is in use. + -- remove the created user if one got made. There is no associated credential. + status = lock_utils.STATUS_DUPLICATE + lock_utils.delete_user(device, active_credential.userIndex) + else + if lock_utils.get_credential(device, credential_index) ~= nil then + -- Credential has been deleted. + lock_utils.delete_credential(device, credential_index) + emit_event = true + end + end + + if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then + -- the credential we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME) + if (credential_index >= last_slot) then + device:set_field(lock_utils.CHECKING_CODE, nil) + emit_event = true + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end + + if emit_event then + lock_utils.send_events(device) + end + -- ignore handling the busy state for these commands, they are handled within their own handlers + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, status) + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local command = device:get_field(lock_utils.COMMAND_NAME) + local emit_events = false + + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code updated + device:emit_event(capabilities.lockCredentials.commandResult( + {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, + { state_change = true, visibility = { displayed = true } } + )) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All credentials deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + emit_events = true + end + else + -- One credential deleted + if (lock_utils.get_credential(device, credential_index) ~= nil) then + lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, + user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_events = true + end + end + end + + if emit_events then + lock_utils.send_events(device) + end +end + +-- REMOVE THIS AFTER DONE WITH TESTING +local migrate = function(driver, device, value) + log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. + "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. + "\n --------------------------------- \n") +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockUsers.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code " .. code_id + local user = lock_utils.get_user(device, code_id) + if user ~= nil then + code_name = user.userName + end + + event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + + +local new_capabilities_driver = { + NAME = "Lock Driver Using New Capabilities", + supported_capabilities = { + Lock, + LockCredentials, + LockUsers, + Battery, + }, + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler, + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.MaxPINCodeLength.ID] = max_code_length_handler, + [LockCluster.attributes.MinPINCodeLength.ID] = min_code_length_handler, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = max_codes_handler, + } + } + }, + capability_handlers = { + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + }, + + [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING + [capabilities.lockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = require("using-new-capabilities.sub_drivers"), + health_check = false, + lifecycle_handlers = { + init = init, + doConfigure = do_configure + }, + can_handle = require("using-new-capabilities.can_handle") +} + +return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua new file mode 100644 index 0000000000..f8fcae368f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,413 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Driver utilities +local device_management = require "st.zigbee.device_management" + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCodes = capabilities.lockCodes +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(driver, device, command) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end + device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = { codeName = lock_utils.get_code_name(device, code_slot) } + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + -- Code slot is occupied + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + else + -- Code slot is unoccupied + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + -- Code has been deleted + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + else + -- Code is unset + event.value = code_slot .. " unset" + device:emit_event(event) + end + end + + code_slot = tonumber(code_slot) + if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then + -- the code we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 + if (code_slot >= last_slot) then + device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) + device:set_field(lock_utils.CHECKING_CODE, nil) + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {} + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code changed + event.value = "0 set" + event.data = { codeName = "Master Code" } + device:emit_event(event) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All codes deleted + for cs, _ in pairs(lock_utils.get_lock_codes(device)) do + lock_utils.code_deleted(device, cs) + end + lock_utils.lock_codes_event(device, {}) + else + -- One code deleted + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + -- Code added or changed + local change_type = lock_utils.get_change_type(device, code_slot) + local code_name = lock_utils.get_code_name(device, code_slot) + event.value = code_slot .. change_type + event.data = { codeName = code_name } + device:emit_event(event) + if (change_type == " set") then + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + end +end + +local handle_max_codes = function(driver, device, value) + if value.value ~= 0 then + -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, + -- but it gave us a non-zero number of pin users, so we want to switch the profile + if not device:supports_capability_by_id(LockCodes.ID) then + device:try_update_metadata({ profile = "base-lock" }) -- switch to a lock with codes + lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes + if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes + -- so we'll load them manually + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end + end + device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) + end +end + +local handle_max_code_length = function(driver, device, value) + device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) +end + +local handle_min_code_length = function(driver, device, value) + device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code " .. code_id + local lock_codes = device:get_field("lockCodes") + if (lock_codes ~= nil and + lock_codes[code_id] ~= nil) then + code_name = lock_codes[code_id] + end + event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + +local update_codes = function(driver, device, command) + local delay = 0 + -- args.codes is json + for name, code in pairs(command.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.SetPINCode(device, + code_slot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + code)) + end) + delay = delay + 2 + else + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) + end) + delay = delay + 2 + end + device.thread:call_with_delay(delay, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) + end) + delay = delay + 2 + end + end +end + +local delete_code = function(driver, device, command) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) +end + +local request_code = function(driver, device, command) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) +end + +local set_code = function(driver, device, command) + if (command.args.codePIN == "") then + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = { command.args.codeSlot, command.args.codeName } + }) + else + device:send(LockCluster.server.commands.SetPINCode(device, + command.args.codeSlot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.codePIN) + ) + if (command.args.codeName ~= nil) then + -- wait for confirmation from the lock to commit this to memory + -- Groovy driver has a lot more info passed here as a description string, may need to be investigated + local codeState = device:get_field(lock_utils.CODE_STATE) or {} + codeState["setName" .. command.args.codeSlot] = command.args.codeName + device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) + end + + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) + end +end + +local name_slot = function(driver, device, command) + local code_slot = tostring(command.args.codeSlot) + local lock_codes = lock_utils.get_lock_codes(device) + if (lock_codes[code_slot] ~= nil) then + lock_codes[code_slot] = command.args.codeName + device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) + lock_utils.lock_codes_event(device, lock_codes) + end +end + +local migrate = function(driver, device, command) + local lock_users = {} + local lock_credentials = {} + local lock_codes = lock_utils.get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index, code_slot in ipairs(ordered_codes) do + table.insert(lock_users, { userIndex = index, userType = "guest", userName = lock_codes[code_slot] }) + table.insert(lock_credentials, { userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin" }) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.maxCodeLength.NAME, 8) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 0) + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(LockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + device:emit_event(LockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(LockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } })) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + -- Don't send a reload all codes if this is a part of migration + if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then + device.thread:call_with_delay(2, function(d) + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end) + else + device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) + end +end + +local old_capabilities_driver = { + NAME = "Lock Driver Using Old Capabilities", + supported_capabilities = { + Lock, + LockCodes, + Battery, + }, + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, + [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes, + } + } + }, + capability_handlers = { + [LockCodes.ID] = { + [LockCodes.commands.updateCodes.NAME] = update_codes, + [LockCodes.commands.deleteCode.NAME] = delete_code, + [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [LockCodes.commands.requestCode.NAME] = request_code, + [LockCodes.commands.setCode.NAME] = set_code, + [LockCodes.commands.nameSlot.NAME] = name_slot, + [LockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = { + require("using-old-capabilities.sub_drivers") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = require("using-old-capabilities.can_handle") +} + +return old_capabilities_driver diff --git a/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml index 5fbfb13f3d..e3a25ab57a 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: tamperAlert diff --git a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml index f4957f9ad0..efb97c8b27 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: refresh diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 925452c431..985daaa92e 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -8,21 +8,15 @@ local cc = require "st.zwave.CommandClass" local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.defaults local defaults = require "st.zwave.defaults" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.Time -local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) -local constants = require "st.zwave.constants" -local utils = require "st.utils" -local json = require "st.json" + +local do_refresh = function(self, device) + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) +end local SCAN_CODES_CHECK_INTERVAL = 30 -local MIGRATION_COMPLETE = "migrationComplete" -local MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped" local function periodic_codes_state_verification(driver, device) local scan_codes_state = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME) @@ -42,100 +36,38 @@ local function periodic_codes_state_verification(driver, device) end end -local function populate_state_from_data(device) - if device.data.lockCodes ~= nil and device:get_field(MIGRATION_COMPLETE) ~= true then - -- build the lockCodes table - local lockCodes = {} - local lc_data = json.decode(device.data.lockCodes) - for k, v in pairs(lc_data) do - lockCodes[k] = v - end - -- Populate the devices `lockCodes` field - device:set_field(constants.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) - -- Populate the devices state history cache - device.state_cache["main"] = device.state_cache["main"] or {} - device.state_cache["main"][capabilities.lockCodes.ID] = device.state_cache["main"][capabilities.lockCodes.ID] or {} - device.state_cache["main"][capabilities.lockCodes.ID][capabilities.lockCodes.lockCodes.NAME] = {value = json.encode(utils.deep_copy(lockCodes))} - - device:set_field(MIGRATION_COMPLETE, true, { persist = true }) - end -end - ---- Builds up initial state for the device ---- ---- @param self st.zwave.Driver ---- @param device st.zwave.Device -local function added_handler(self, device) - populate_state_from_data(device) - if device.data.lockCodes == nil or device:get_field(MIGRATION_RELOAD_SKIPPED) == true then - if (device:supports_capability(capabilities.lockCodes)) then - self:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} }) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(self, device) - end - ) +local do_added = function(driver, device) + -- this variable should only be present for test cases trying to test the old capabilities. + if device.useOldCapabilityForTesting == true then + -- added handler from using old capabilities + driver:inject_capability_command(device, + { capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} }) + device.thread:call_with_delay( + SCAN_CODES_CHECK_INTERVAL, + function() + periodic_codes_state_verification(driver, device) + end + ) + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) + if (device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) end else - device:set_field(MIGRATION_RELOAD_SKIPPED, true, { persist = true }) - end - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) - if (device:supports_capability(capabilities.tamperAlert)) then - device:emit_event(capabilities.tamperAlert.tamper.clear()) - end -end - -local init_handler = function(driver, device, event) - populate_state_from_data(device) - -- temp fix before this can be changed from being persisted in memory - device:set_field(constants.CODE_STATE, nil, { persist = true }) -end - -local do_refresh = function(self, device) - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) -end - ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param cmd table -local function update_codes(driver, device, cmd) - local delay = 0 - -- args.codes is json - for name, code in pairs(cmd.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - -- code changed - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({ - user_identifier = code_slot, - user_code = code, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) - end) - delay = delay + 2.2 - else - -- code deleted - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) - end) - delay = delay + 2.2 - device.thread:call_with_delay(delay, function () - device:send(UserCode:Get({user_identifier = code_slot})) - end) - delay = delay + 2.2 - end + if device:supports_capability_by_id(capabilities.lockCodes.ID) then + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + -- make the driver call this command again, it will now be handled in new capabilities. + driver.lifecycle_dispatcher:dispatch(driver, device, "added") end end end local function time_get_handler(driver, device, cmd) + local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) local time = os.date("*t") device:send_to_component( Time:Report({ @@ -151,24 +83,22 @@ local driver_template = { supported_capabilities = { capabilities.lock, capabilities.lockCodes, + capabilities.lockUsers, + capabilities.lockCredentials, capabilities.battery, capabilities.tamperAlert }, lifecycle_handlers = { - added = added_handler, - init = init_handler, + added = do_added }, capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes - }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, zwave_handlers = { [cc.TIME] = { - [Time.GET] = time_get_handler -- used by DanaLock + [0x01] = time_get_handler -- used by DanaLock } }, sub_drivers = require("sub_drivers"), diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua index 45115081e4..cadcf6c928 100644 --- a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -1,7 +1,6 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - return function(sub_driver_name) -- gets the current lua libs api version local ZwaveDriver = require "st.zwave.driver" @@ -14,5 +13,4 @@ return function(sub_driver_name) else return require(sub_driver_name) end - end diff --git a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua new file mode 100644 index 0000000000..ea1c4c45c8 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua @@ -0,0 +1,489 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local utils = require "st.utils" +local INITIAL_INDEX = 1 + +local new_lock_utils = { + -- Constants + ADD_CREDENTIAL = "addCredential", + ADD_USER = "addUser", + BUSY = "busy", + COMMAND_NAME = "commandName", + CREDENTIAL_TYPE = "pin", + CHECKING_CODE = "checkingCode", + DELETE_ALL_CREDENTIALS = "deleteAllCredentials", + DELETE_ALL_USERS = "deleteAllUsers", + DELETE_CREDENTIAL = "deleteCredential", + DELETE_USER = "deleteUser", + LOCK_CREDENTIALS = "lockCredentials", + LOCK_USERS = "lockUsers", + ACTIVE_CREDENTIAL = "pendingCredential", + STATUS_BUSY = "busy", + STATUS_DUPLICATE = "duplicate", + STATUS_FAILURE = "failure", + STATUS_INVALID_COMMAND = "invalidCommand", + STATUS_OCCUPIED = "occupied", + STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", + STATUS_SUCCESS = "success", + TABLES_LOADED = "tablesLoaded", + UPDATE_CREDENTIAL = "updateCredential", + UPDATE_USER = "updateUser", + USER_INDEX = "userIndex", + USER_NAME = "userName", + USER_TYPE = "userType" +} + +-- check if we are currently busy performing a task. +-- if we aren't then set as busy. +new_lock_utils.busy_check_and_set = function (device, command, override_busy_check) + if override_busy_check then + -- the function was called by an injected command. + return false + end + + local c_time = os.time() + local busy_state = device:get_field(new_lock_utils.BUSY) or false + + if busy_state == false or c_time - busy_state > 10 then + device:set_field(new_lock_utils.COMMAND_NAME, command) + device:set_field(new_lock_utils.BUSY, c_time) + return false + else + local command_result_info = { + commandName = command.name, + statusCode = new_lock_utils.STATUS_BUSY + } + if command.type == new_lock_utils.LOCK_USERS then + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + else + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + end + return true + end +end + +new_lock_utils.clear_busy_state = function(device, status, override_busy_check) + if override_busy_check then + return + end + local command = device:get_field(new_lock_utils.COMMAND_NAME) + local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + if command ~= nil then + local command_result_info = { + commandName = command.name, + statusCode = status + } + if command.type == new_lock_utils.LOCK_USERS then + if active_credential ~= nil and active_credential.userIndex ~= nil then + command_result_info.userIndex = active_credential.userIndex + end + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + else + if active_credential ~= nil and active_credential.userIndex ~= nil then + command_result_info.userIndex = active_credential.userIndex + end + if active_credential ~= nil and active_credential.credentialIndex ~= nil then + command_result_info.credentialIndex = active_credential.credentialIndex + end + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + end + end + + device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, nil) + device:set_field(new_lock_utils.COMMAND_NAME, nil) + device:set_field(new_lock_utils.BUSY, false) +end + +new_lock_utils.reload_tables = function(device) + local users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.users.NAME, {}) + local credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.credentials.NAME, {}) + if next(users) ~= nil then + device:set_field(new_lock_utils.LOCK_USERS, users) + end + if next(credentials) ~= nil then + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + end + + device:set_field(new_lock_utils.TABLES_LOADED, true) +end + +new_lock_utils.get_users = function(device) + if not device:get_field(new_lock_utils.TABLES_LOADED) then + new_lock_utils.reload_tables(device) + end + + local users = utils.deep_copy(device:get_field(new_lock_utils.LOCK_USERS)) + return users ~= nil and users or {} +end + +new_lock_utils.get_user = function(device, user_index) + for _, user in pairs(new_lock_utils.get_users(device)) do + if user.userIndex == user_index then + return user + end + end + + return nil +end + +new_lock_utils.get_available_user_index = function(device) + local max = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 8) + local current_users = new_lock_utils.get_users(device) + local available_index = nil + local used_index = {} + for _, user in pairs(current_users) do + used_index[user.userIndex] = true + end + if current_users ~= {} then + for index = 1, max do + if used_index[index] == nil then + available_index = index + break + end + end + else + available_index = INITIAL_INDEX + end + return available_index +end + +new_lock_utils.get_credentials = function(device) + if not device:get_field(new_lock_utils.TABLES_LOADED) then + new_lock_utils.reload_tables(device) + end + + local credentials = utils.deep_copy(device:get_field(new_lock_utils.LOCK_CREDENTIALS)) + return credentials ~= nil and credentials or {} +end + +new_lock_utils.get_credential = function(device, credential_index) + for _, credential in pairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential + end + end + return nil +end + +new_lock_utils.get_credential_by_user_index = function(device, user_index) + for _, credential in pairs(new_lock_utils.get_credentials(device)) do + if credential.userIndex == user_index then + return credential + end + end + + return nil +end + +new_lock_utils.get_available_credential_index = function(device) + local max = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME, 8) + local current_credentials = new_lock_utils.get_credentials(device) + local available_index = nil + local used_index = {} + for _, credential in pairs(current_credentials) do + used_index[credential.credentialIndex] = true + end + if current_credentials ~= {} then + for index = 1, max do + if used_index[index] == nil then + available_index = index + break + end + end + else + available_index = INITIAL_INDEX + end + return available_index +end + +new_lock_utils.create_user = function(device, user_name, user_type, user_index) + if user_name == nil then + user_name = "Guest" .. user_index + end + + local current_users = new_lock_utils.get_users(device) + table.insert(current_users, { userIndex = user_index, userType = user_type, userName = user_name }) + device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) +end + +new_lock_utils.delete_user = function(device, user_index) + local current_users = new_lock_utils.get_users(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, user in pairs(current_users) do + if user.userIndex == user_index then + -- table.remove causes issues if we are removing while iterating. + -- instead set the value as nil and let `prep_table` handle removing it. + current_users[index] = nil + device:set_field(new_lock_utils.LOCK_USERS, current_users) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +new_lock_utils.add_credential = function(device, user_index, credential_type, credential_index) + local credentials = new_lock_utils.get_credentials(device) + table.insert(credentials, + { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + return new_lock_utils.STATUS_SUCCESS +end + +new_lock_utils.delete_credential = function(device, credential_index) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, credential in pairs(credentials) do + if credential.credentialIndex == credential_index then + new_lock_utils.delete_user(device, credential.userIndex) + -- table.remove causes issues if we are removing while iterating. + -- instead set the value as nil and let `prep_table` handle removing it. + credentials[index] = nil + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for _, credential in pairs(credentials) do + if credential.credentialIndex == credential_index then + credential.credentialType = credential_type + credential.userIndex = user_index + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +-- emit_event doesn't like having `nil` values in the table. Remove any if they are present. +new_lock_utils.prep_table = function(data) + local clean_table = {} + for _, value in pairs(data) do + if value ~= nil then + clean_table[#clean_table + 1] = value -- Append to the end of the new array + end + end + return clean_table +end + +new_lock_utils.send_events = function(device, type) + if type == nil or type == new_lock_utils.LOCK_USERS then + local current_users = new_lock_utils.prep_table(new_lock_utils.get_users(device)) + device:emit_event(capabilities.lockUsers.users(current_users, + {state_change = true, visibility = { displayed = true } })) + end + if type == nil or type == new_lock_utils.LOCK_CREDENTIALS then + local credentials = new_lock_utils.prep_table(new_lock_utils.get_credentials(device)) + device:emit_event(capabilities.lockCredentials.credentials(credentials, + { state_change = true, visibility = { displayed = true } })) + end +end + +new_lock_utils.get_code_id_from_notification_event = function(event_params, v1_alarm_level) + -- some locks do not properly include the code ID in the event params, but do encode it + -- in the v1 alarm level + local code_id = v1_alarm_level + if event_params ~= nil and event_params ~= "" then + event_params = {event_params:byte(1,-1)} + code_id = (#event_params == 1) and event_params[1] or event_params[3] + end + return tostring(code_id) +end + +-- This is the part of the notifcation event handler code from the base driver +-- that deals with lock code programming events +new_lock_utils.base_driver_code_event_handler = function(driver, device, cmd) + local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) + local access_control_event = Notification.event.access_control + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event = cmd.args.event + local credential_index = tonumber(new_lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level)) + local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + local status = new_lock_utils.STATUS_SUCCESS + local command = device:get_field(new_lock_utils.COMMAND_NAME) + local emit_event = false + + if (event == access_control_event.ALL_USER_CODES_DELETED) then + -- all credentials have been deleted + for _, credential in pairs(new_lock_utils.get_credentials(device)) do + new_lock_utils.delete_credential(device, credential.credentialIndex) + emit_event = true + end + elseif (event == access_control_event.SINGLE_USER_CODE_DELETED) then + -- credential has been deleted. + if new_lock_utils.get_credential(device, credential_index) ~= nil then + new_lock_utils.delete_credential(device, credential_index) + emit_event = true + end + elseif (event == access_control_event.NEW_USER_CODE_ADDED) then + if command ~= nil and command.name == new_lock_utils.ADD_CREDENTIAL then + -- create credential if not already present. + if new_lock_utils.get_credential(device, credential_index) == nil then + new_lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_event = true + end + elseif command ~= nil and command.name == new_lock_utils.UPDATE_CREDENTIAL then + -- update credential + local credential = new_lock_utils.get_credential(device, credential_index) + if credential ~= nil then + new_lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_event = true + end + else + -- out-of-band update. Don't add if already in table. + if new_lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = new_lock_utils.get_available_user_index(device) + if new_user_index ~= nil then + new_lock_utils.create_user(device, nil, "guest", new_user_index) + new_lock_utils.add_credential(device, + new_user_index, + new_lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_event = true + else + status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + end + elseif (event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE) then + -- adding credential failed since code already exists. + -- remove the created user if one got made. There is no associated credential. + status = new_lock_utils.STATUS_DUPLICATE + if active_credential ~= nil then new_lock_utils.delete_user(device, active_credential.userIndex) end + elseif (event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION) then + -- master code changed -- should we send an index with this? + device:emit_event(capabilities.lockCredentials.commandResult( + {commandName = new_lock_utils.UPDATE_CREDENTIAL, statusCode = new_lock_utils.STATUS_SUCCESS}, + { state_change = true, visibility = { displayed = true } } + )) + end + + -- handle emitting events if any changes occured. + if emit_event then + new_lock_utils.send_events(device) + end + -- clear the busy state and handle the commandStatus + -- ignore handling the busy state for some commands, they are handled within their own handlers + if command ~= nil and command ~= new_lock_utils.DELETE_ALL_CREDENTIALS and command ~= new_lock_utils.DELETE_ALL_USERS then + new_lock_utils.clear_busy_state(device, status) + end + end +end + +new_lock_utils.door_operation_event_handler = function(driver, device, cmd) + local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) + local access_control_event = Notification.event.access_control + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event = cmd.args.event + if (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then + local event_to_send + + local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" + } + + local DELAY_LOCK_EVENT = "_delay_lock_event" + local DELAY_LOCK_EVENT_TIMER = "_delay_lock_event_timer" + local MAX_DELAY = 10 + + if ((event >= access_control_event.MANUAL_LOCK_OPERATION and + event <= access_control_event.KEYPAD_UNLOCK_OPERATION) or + event == access_control_event.AUTO_LOCK_LOCKED_OPERATION) then + -- even event codes are unlocks, odd event codes are locks + local events = {[0] = capabilities.lock.lock.unlocked(), [1] = capabilities.lock.lock.locked()} + event_to_send = events[event & 1] + elseif (event >= access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION and + event <= access_control_event.LOCK_JAMMED) then + event_to_send = capabilities.lock.lock.unknown() + end + + if (event_to_send ~= nil) then + local method_map = { + [access_control_event.MANUAL_UNLOCK_OPERATION] = METHOD.MANUAL, + [access_control_event.MANUAL_LOCK_OPERATION] = METHOD.MANUAL, + [access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION] = METHOD.MANUAL, + [access_control_event.RF_LOCK_OPERATION] = METHOD.COMMAND, + [access_control_event.RF_UNLOCK_OPERATION] = METHOD.COMMAND, + [access_control_event.RF_NOT_FULLY_LOCKED_OPERATION] = METHOD.COMMAND, + [access_control_event.KEYPAD_LOCK_OPERATION] = METHOD.KEYPAD, + [access_control_event.KEYPAD_UNLOCK_OPERATION] = METHOD.KEYPAD, + [access_control_event.AUTO_LOCK_LOCKED_OPERATION] = METHOD.AUTO, + [access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION] = METHOD.AUTO + } + + event_to_send["data"] = {method = method_map[event]} + + -- SPECIAL CASES: + if (event == access_control_event.MANUAL_UNLOCK_OPERATION and cmd.args.event_parameter == 2) then + -- functionality from DTH, some locks can distinguish being manually locked via keypad + event_to_send.data.method = METHOD.KEYPAD + elseif (event == access_control_event.KEYPAD_LOCK_OPERATION or event == access_control_event.KEYPAD_UNLOCK_OPERATION) then + local code_id = cmd.args.v1_alarm_level + if cmd.args.event_parameter ~= nil and string.len(cmd.args.event_parameter) ~= 0 then + local event_params = { cmd.args.event_parameter:byte(1, -1) } + code_id = (#event_params == 1) and event_params[1] or event_params[3] + end + local user_id = nil + local credential = new_lock_utils.get_credential(device, code_id) + if (credential ~= nil) then + user_id = credential.userIndex + end + if user_id ~= nil then event_to_send["data"] = { userIndex = user_id, method = event_to_send["data"].method } end + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + "main", + capabilities.lock.ID, + capabilities.lock.lock.ID) == event_to_send.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local socket = require "socket" + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + local timer = device:get_field(DELAY_LOCK_EVENT_TIMER) + if timer ~= nil then + device.thread:cancel_timer(timer) + device:set_field(DELAY_LOCK_EVENT_TIMER, nil) + end + + device:emit_event(event_to_send) + end + end + end +end + +return new_lock_utils diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua index 46700ce154..0e412d3da9 100644 --- a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -3,10 +3,8 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("zwave-alarm-v1-lock"), - lazy_load_if_possible("schlage-lock"), - lazy_load_if_possible("samsung-lock"), - lazy_load_if_possible("keywe-lock"), + lazy_load_if_possible("using-old-capabilities"), + lazy_load_if_possible("using-new-capabilities"), lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index 09d59f4861..206e364669 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" @@ -28,13 +27,16 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, zwave_product_type = KEYWE_PRODUCT_TYPE, - zwave_product_id = KEYWE_PRODUCT_ID + zwave_product_id = KEYWE_PRODUCT_ID, + useOldCapabilityForTesting = true, } ) +-- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) test.register_coroutine_test( diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua new file mode 100644 index 0000000000..463a968dad --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua @@ -0,0 +1,128 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + +local KEYWE_MANUFACTURER_ID = 0x037B +local KEYWE_PRODUCT_TYPE = 0x0002 +local KEYWE_PRODUCT_ID = 0x0001 + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = DoorLock} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, + zwave_product_type = KEYWE_PRODUCT_TYPE, + zwave_product_id = KEYWE_PRODUCT_ID, + } +) + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + +test.register_coroutine_test( + "Door Lock Operation Reports unlocked should be handled", + function() + added() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0x00}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) + end +) + +test.register_coroutine_test( + "Door Lock Operation Reports locked should be handled", + function() + added() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0xFF}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) + end +) + +test.register_coroutine_test( + "Lock notification reporting should be handled", + function() + added() + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 24}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 25}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) + -- not a special case for this lock, should be handled as usual + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 6, event_parameter = "\x01"}) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index 11c03650c6..e32fe5aeb2 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -42,7 +41,8 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = DANALOCK_MANUFACTURER_ID, zwave_product_type = DANALOCK_PRODUCT_TYPE, - zwave_product_id = DANALOCK_PRODUCT_ID + zwave_product_id = DANALOCK_PRODUCT_ID, + useOldCapabilityForTesting = true, } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 81ba1df2ad..437c1cb16a 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local json = require "dkjson" @@ -20,7 +19,8 @@ local mock_device = test.mock_device.build_test_zwave_device( profile = t_utils.get_profile_definition("base-lock.yml"), zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID + zwave_product_id = SAMSUNG_PRODUCT_ID, + useOldCapabilityForTesting = true, } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua new file mode 100644 index 0000000000..ec68657690 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua @@ -0,0 +1,263 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) +local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local lock_utils = require "new_lock_utils" + +local SAMSUNG_MANUFACTURER_ID = 0x022E +local SAMSUNG_PRODUCT_TYPE = 0x0001 +local SAMSUNG_PRODUCT_ID = 0x0001 + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, + zwave_product_type = SAMSUNG_PRODUCT_TYPE, + zwave_product_id = SAMSUNG_PRODUCT_ID, + } +) + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + +local function init_code_slot(slot_number, name, device) + local credentials = device.transient_store[lock_utils.LOCK_CREDENTIALS] + local users = device.transient_store[lock_utils.LOCK_USERS] + if credentials == nil then + credentials = {} + device.transient_store[lock_utils.LOCK_CREDENTIALS] = credentials + end + if users == nil then + users = {} + device.transient_store[lock_utils.LOCK_USERS] = users + end + table.insert(credentials, { userIndex = slot_number, credentialIndex = slot_number, credentialType = "pin" }) + table.insert(users, { userIndex = slot_number, userName = name, userType = "guest" }) +end + +test.register_coroutine_test( + "When the device is added an unlocked event should be sent", + function() + added() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = "" } + ) + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({user_identifier = 1}) + ) + ) + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Notification about correctly added code should be handled", + function() + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "All user codes should be reported as deleted upon changing Master Code", + function() + added() + init_code_slot(1, "Code 1", mock_device) + init_code_slot(2, "Code 2", mock_device) + init_code_slot(3, "Code 3", mock_device) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({ + { userIndex = 1, userName = "new name", userType = "guest" }, + { userIndex = 2, userName = "Code 2", userType = "guest" }, + { userIndex = 3, userName = "Code 3", userType = "guest" } + }, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, + event_parameter = "" } + ) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index 38dabbd9dd..4f17c62bf7 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -36,12 +35,14 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID + zwave_product_id = SCHLAGE_PRODUCT_ID, + useOldCapabilityForTesting = true, } ) local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} +-- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua new file mode 100644 index 0000000000..34356601e9 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua @@ -0,0 +1,298 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + +local SCHLAGE_MANUFACTURER_ID = 0x003B +local SCHLAGE_PRODUCT_TYPE = 0x0002 +local SCHLAGE_PRODUCT_ID = 0x0469 + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, + zwave_product_type = SCHLAGE_PRODUCT_TYPE, + zwave_product_id = SCHLAGE_PRODUCT_ID, + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + added() + test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number}) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(4.2) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Configuration report should be handled", + function() + added() + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + ) + end +) + +test.register_coroutine_test( + "Configuration report indicating code deletion should be handled", + function() + added() + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 4 + }) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) + ) + end +) + +test.register_coroutine_test( + "User code report indicating master code is available should indicate code deletion", + function() + added() + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 0, + user_id_status = UserCode.user_id_status.AVAILABLE + }) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + end +) + +test.register_coroutine_test( + "Device should send appropriate configuration messages", + function() + added() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number + }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Association:Set({ + grouping_identifier = 2, + node_ids = {} + }) + ) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Basic Sets should result in an Association remove", + function() + added() + test.socket.zwave:__queue_receive({ + mock_device.id, + Basic:Set({ + value = 0x00 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({})) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Association:Remove({ + grouping_identifier = 1, + node_ids = {} + }) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index c798feca08..9f26bbf7ef 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -33,10 +32,11 @@ local zwave_lock_endpoints = { } local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + useOldCapabilityForTesting = true, + } ) local function test_init() @@ -502,7 +502,7 @@ test.register_coroutine_test( Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, - event_parameter = "" + event_parameter = "\x01" }) } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua new file mode 100644 index 0000000000..81946aa759 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -0,0 +1,127 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +local t_utils = require "integration_test.utils" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" + +local SCHLAGE_MANUFACTURER_ID = 0x003B +local SCHLAGE_PRODUCT_TYPE = 0x0002 +local SCHLAGE_PRODUCT_ID = 0x0469 + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION } + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + useOldCapabilityForTesting = true, + } +) + +local schlage_mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, + zwave_product_type = SCHLAGE_PRODUCT_TYPE, + zwave_product_id = SCHLAGE_PRODUCT_ID, + useOldCapabilityForTesting = true, + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(schlage_mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + init_code_slot(1, "Zach", mock_device) + init_code_slot(5, "Steven", mock_device) + -- setup codes + test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Migrate new device", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Schlage-Lock device called 'migrate' command, validate codeLength is being properly set", + function() + init_code_slot(1, "Zach", schlage_mock_device) + init_code_slot(5, "Steven", schlage_mock_device) + -- setup codes + test.socket.zwave:__queue_receive({schlage_mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.zwave:__queue_receive({schlage_mock_device.id, Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })}) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ schlage_mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua new file mode 100644 index 0000000000..73057ac7c8 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua @@ -0,0 +1,730 @@ +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +local t_utils = require "integration_test.utils" +local access_control_event = Notification.event.access_control + + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} +local test_credential_index = 1 +local test_credentials = {} +local test_users = {} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } +) + +-- if user_index is 0 it creates a new user. +local function add_credential(user_index) + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", "123" .. test_credential_index } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = test_credential_index, + user_code = "123" .. test_credential_index, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + local payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" + payload = payload:sub(1, 1) .. string.char(test_credential_index) .. payload:sub(3) + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = payload + }) + }) + table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(test_users, + { state_change = true, visibility = { displayed = true } }) + ) + ) + table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials(test_credentials, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + test_credential_index = test_credential_index + 1 +end + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) + + -- reset these globals + test_credential_index = 1 + test_credentials = {} + test_users = {} +end + +test.set_test_init_function(test_init) + +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + +test.register_coroutine_test( + "Add user should succeed", + function() + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Add credential should succeed", + function() + added() + -- these all should succeed + add_credential(0) + add_credential(0) + add_credential(0) + end +) + +test.register_coroutine_test( + "Add credential for existing user should succeed", + function() + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "Guest1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Guest1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + + -- add credential with the new users index (1). + add_credential(1) + end +) + +test.register_coroutine_test( + "Update user should succeed", + function() + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "new name" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete user should succeed", + function() + added() + -- add credential + add_credential(0) + + -- delete the user which should also delete the credential + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { 1 } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + {}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Update credential should succeed", + function() + added() + -- add credential + add_credential(0) + + -- update the credential + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { 1, 1, "pin", "3456" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "3456", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" -- update payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete credential should succeed", + function() + added() + -- add the credential + add_credential(0) + + -- -- delete the credential + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 1, "pin" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + {}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1, }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Delete all users should succeed", + function() + added() + -- add credential + add_credential(0) + -- add second credential + add_credential(0) + + -- delete all users. This should also delete the two associated credentials + test.socket.capability:__queue_receive({mock_device.id, + { + 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.mock_time.advance_time(0) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.mock_time.advance_time(0.5) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 2, userName = "Guest2", userType = "guest" } + }, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, + { state_change = true, visibility = { displayed = true } }) + ) + ) + + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success"}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code number", + function() + added() + -- add credential + add_credential(0) + -- send unlock + test.socket.zwave:__queue_receive( + { + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = "\x01" + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", userIndex = 1 } }) + ) + ) + end +) + +test.register_coroutine_test( + "Creating a credential should succeed if the lock responds with a user code report", + function() + added() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 0, "guest", "pin", "123" .. test_credential_index } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = test_credential_index, + user_code = "123" .. test_credential_index, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + })}) + table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(test_users, + { state_change = true, visibility = { displayed = true } }) + ) + ) + table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials(test_credentials, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + test_credential_index = test_credential_index + 1 + end +) + +test.register_coroutine_test( + "Lock alarm reporting should be handled", + function() + added() + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 22, alarm_level = 1})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 9})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown())) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 19, alarm_level = 3})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 18, alarm_level=0})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 2})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 1})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 23})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 24})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 25})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 26})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="auto"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 27})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="auto"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 32})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 13, alarm_level = 5})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({ { userIndex = 1, userName = "Guest1", userType = "guest"} }, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials( { {userIndex = 1, credentialIndex = 5, credentialType = "pin" } }, { state_change = true, visibility = { displayed = true } }))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 34, alarm_level = 2})}) + -- no op because we have no active operation + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 161})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected())) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 168})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(1))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 169})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua new file mode 100644 index 0000000000..5618b59b30 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local capabilities = require "st.capabilities" + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + local subdriver = require("using-new-capabilities") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua new file mode 100644 index 0000000000..e4241e9f03 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,418 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local LockUsers = capabilities.lockUsers +local LockCredentials = capabilities.lockCredentials +local lock_utils = require "new_lock_utils" +local utils = require "st.utils" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local log = require "log" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" + +-- Helper methods +local reload_all_codes = function(device) + local max_codes = device:get_latest_state("main", + LockCredentials.ID, LockCredentials.pinUsersSupported.NAME) + if (max_codes == nil) then + device:send(UserCode:UsersNumberGet({})) + end + + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) + end + + device:send(UserCode:Get({user_identifier = device:get_field(lock_utils.CHECKING_CODE)})) +end + +-- Lifecycle handlers +local added_handler = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(2, function () + reload_all_codes(device) + end) + -- read user/credential metadata + -- reload all codes + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) + if (device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) +end + +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(10, function () + reload_all_codes(device) + end) +end + +-- Lock Users commands +local add_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then + return + end + local available_index = lock_utils.get_available_user_index(device) + local status = lock_utils.STATUS_SUCCESS + if available_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + else + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) + lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) + end + + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) + end + + lock_utils.clear_busy_state(device, status) +end + +local update_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then + return + end + + local user_name = command.args.userName + local user_type = command.args.userType + local user_index = tonumber(command.args.userIndex) + local current_users = lock_utils.get_users(device) + local status = lock_utils.STATUS_FAILURE + + for _, user in pairs(current_users) do + if user.userIndex == user_index then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break + end + end + + lock_utils.clear_busy_state(device, status) +end + +local delete_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then + return + end + local status = lock_utils.STATUS_SUCCESS + local user_index = tonumber(command.args.userIndex) + if lock_utils.get_user(device, user_index) ~= nil then + + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + end + + local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) + if associated_credential ~= nil then + -- if there is an associated credential with this user then delete the credential + -- this command also handles the user deletion + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { associated_credential.credentialIndex, "pin" }, + override_busy_check = true + }) + else + lock_utils.delete_user(device, user_index) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end + else + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end +end + +local delete_all_users_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then + return + end + local status = lock_utils.STATUS_SUCCESS + local current_users = lock_utils.get_users(device) + + local delay = 0 + for _, user in pairs(current_users) do + device.thread:call_with_delay(delay, function() + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = {user.userIndex}, + override_busy_check = true + }) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +--- Lock Credentials Commands + +local add_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local user_index = tonumber(command.args.userIndex) + local user_type = command.args.userType + local credential_type = command.args.credentialType + local credential_data = command.args.credentialData + local status = lock_utils.STATUS_SUCCESS + + local credential_index = lock_utils.get_available_credential_index(device) + if credential_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then + status = lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then + status = lock_utils.STATUS_FAILURE + end + + if user_index == 0 then + user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, user_type, user_index) + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + + if status == lock_utils.STATUS_SUCCESS then + -- set the pin code and then validate it was successful when the GetPINCode response is received. + -- the credential creation and events will also be handled in that response. + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) + + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = credential_data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + -- clearing busy state handled in user_code_report_handler + else + lock_utils.clear_busy_state(device, status) + end +end + +local update_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credential_index = tonumber(command.args.credentialIndex) + local credential_data = command.args.credentialData + local credential = lock_utils.get_credential(device, credential_index) + + if credential ~= nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = credential_data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + -- clearing busy state handled in user_code_report_handler + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) + end +end + +local delete_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then + return + end + + local credential_index = tonumber(command.args.credentialIndex) + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + end + device:send(UserCode:Set({ + user_identifier = credential.credentialIndex, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + -- clearing busy state handled in user_code_report_handler + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) + end +end + +local delete_all_credentials_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS + local delay = 0 + for _, credential in pairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) + device:send(UserCode:Set({ + user_identifier = credential_index, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +-- Z-Wave Message Handlers + +local user_code_report_handler = function(driver, device, cmd) + local credential_index = cmd.args.user_identifier + local command = device:get_field(lock_utils.COMMAND_NAME) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local user_id_status = cmd.args.user_id_status + local emit_events = false + + if (user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or + (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code)) then + -- credential exists on lock, add the credential if it doesn't exist in our table. + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, + user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_events = true + end + elseif command ~= nil then + if command.name == lock_utils.ADD_CREDENTIAL and lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_events = true + elseif command.name == lock_utils.UPDATE_CREDENTIAL then + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_events = true + end + end + end + elseif user_id_status == UserCode.user_id_status.AVAILABLE then + -- credential slot is open. If it exists on our table then remove it. + if lock_utils.get_credential(device, credential_index) ~= nil then + -- Credential has been deleted. + lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + + -- checking code handler + if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then + -- the credential we're checking has arrived + -- local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, + -- capabilities.lockCredentials.pinUsersSupported.NAME) + local last_slot = 8 -- remove this once testing is done + if (credential_index >= last_slot) then + device:set_field(lock_utils.CHECKING_CODE, nil) + emit_events = true + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(UserCode:Get({user_identifier = checkingCode})) + end + end + + if emit_events then + lock_utils.send_events(device) + end + + -- clear the busy state and handle the commandStatus + -- ignore handling the busy state for some commands, they are handled within their own handlers + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, lock_utils.STATUS_SUCCESS) + end +end + +local notification_report_handler = function(driver, device, cmd) + ------------ USER CODE PROGRAMMING EVENTS ------------ + lock_utils.base_driver_code_event_handler(driver, device, cmd) + + ------------ LOCK OPERATION EVENTS ------------ + lock_utils.door_operation_event_handler(driver, device, cmd) + + ------------ TAMPER EVENTS ------------ + -- We have to load and call this manually since we're now overriding notfication handling + -- in this driver + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) +end + +local users_number_report_handler = function(driver, device, cmd) + -- these are the same for Z-Wave + device:emit_event(LockUsers.totalUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) +end + +-- REMOVE THIS AFTER DONE WITH TESTING +local migrate = function(driver, device, value) + log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. + "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. + "\n --------------------------------- \n") +end + +local zwave_lock = { + supported_capabilities = { + capabilities.lock, + capabilities.lockUsers, + capabilities.lockCredentials, + capabilities.battery, + capabilities.tamperAlert + }, + lifecycle_handlers = { + added = added_handler, + init = init, + }, + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler, + [UserCode.USERS_NUMBER_REPORT] = users_number_report_handler, + } + }, + capability_handlers = { + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + }, + + [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING + [capabilities.lockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = require("using-new-capabilities.sub_drivers"), + NAME = "Using new capabilities", + can_handle = require("using-new-capabilities.can_handle") +} + +return zwave_lock \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..3b12277f18 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local KEYWE_MFR = 0x037B + if device.zwave_manufacturer_id == KEYWE_MFR then + local subdriver = require("using-new-capabilities.keywe-lock") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua new file mode 100644 index 0000000000..26873a9309 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua @@ -0,0 +1,69 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Association = (require "st.zwave.CommandClass.Association")({version=2}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control + +local TamperDefaults = require "st.zwave.defaults.tamperAlert" +local lock_utils = require "new_lock_utils" + +local TAMPER_CLEAR_DELAY = 10 + +local function clear_tamper_if_needed(device) + local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) + if current_tamper_state == "detected" then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_OPEN then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_CLOSED then + event = capabilities.lock.lock.locked() + end + if event ~= nil then + event["data"] = {method = "manual"} + end + end + + if event ~= nil then + device:emit_event(event) + else + lock_utils.door_operation_event_handler(self, device, cmd) + lock_utils.base_driver_code_event_handler(self, device, cmd) + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + device.thread:call_with_delay( + TAMPER_CLEAR_DELAY, + function(d) + clear_tamper_if_needed(device) + end + ) + end +end + +local function do_configure(self, device) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local keywe_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Keywe Lock", + can_handle = require("using-new-capabilities.keywe-lock.can_handle"), +} + +return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..d59b49e2e2 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + local subdriver = require("using-new-capabilities.samsung-lock") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua new file mode 100644 index 0000000000..22f4c963eb --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua @@ -0,0 +1,64 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local access_control_event = Notification.event.access_control + +local lock_utils = require "new_lock_utils" + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL and active_credential ~= nil then + device:send(UserCode:Get({ user_identifier = active_credential.credentialIndex })) + return + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- All other codes are deleted when the master code is changed + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + end + lock_utils.send_events(device) + return + end + end + + if event ~= nil then + device:emit_event(event) + else + lock_utils.door_operation_event_handler(self, device, cmd) + lock_utils.base_driver_code_event_handler(self, device, cmd) + end +end + +-- Used doConfigure instead of added to not overwrite parent driver's added_handler +local function do_configure(self, device) + -- taken directly from DTH + -- Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked + device:emit_event(capabilities.lock.lock.unlocked()) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = require("using-new-capabilities.samsung-lock.can_handle"), +} + +return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..4f1428dc5d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + local subdriver = require("using-new-capabilities.schlage-lock") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua new file mode 100644 index 0000000000..d303dbd466 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua @@ -0,0 +1,117 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) + +local lock_utils = require "new_lock_utils" + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local function do_configure(self, device) + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function basic_set_handler(self, device, cmd) + device:emit_event(cmd.args.value == 0 and capabilities.lock.lock.unlocked() or capabilities.lock.lock.locked()) + device:send(Association:Remove({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function configuration_report(self, device, cmd) + local parameter_number = cmd.args.parameter_number + if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then + local reported_code_length = cmd.args.configuration_value + local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + -- when the code length is changed, all the codes have been wiped + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + end + lock_utils.send_events(device) + end + device:emit_event(capabilities.lockCredentials.minPinCodeLen(reported_code_length)) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(reported_code_length)) + end +end + +local function is_user_code_report_mfr_specific(device, cmd) + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local code_id = cmd.args.user_identifier + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_state = device:get_field(constants.CODE_STATE) + return user_code == "**********" or user_code == nil or (code_state ~= nil and code_state["setName"..cmd.args.user_identifier] ~= nil) + else + return (code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE) or + reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE + end +end + +local function user_code_report_handler(self, device, cmd) + local credential_index = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + + if credential_index == 0 and reported_user_id_status == user_id_status.AVAILABLE then + -- master code changed, clear all credentials + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + end + lock_utils.send_events(device) + end + else + local new_capabilities = require "using-new-capabilities" + new_capabilities.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local function add_credential_handler(self, device, cmd) + local DEFAULT_COMMANDS_DELAY = 4.2 + local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) + local base_handler = function() + local new_capabilities = require "using-new-capabilities" + new_capabilities.capability_handlers[capabilities.lockCredentials.ID][capabilities.lockCredentials.commands.addCredential.NAME](self, device, cmd) + end + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, base_handler) + else + base_handler() + end +end + +local schlage_lock = { + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + capability_handlers = { + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = add_credential_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = require("using-new-capabilities.schlage-lock.can_handle"), +} + +return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua new file mode 100644 index 0000000000..4520fbf68f --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("using-new-capabilities.zwave-alarm-v1-lock"), + lazy_load_if_possible("using-new-capabilities.schlage-lock"), + lazy_load_if_possible("using-new-capabilities.samsung-lock"), + lazy_load_if_possible("using-new-capabilities.keywe-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..f622dcf569 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + local subdriver = require("using-new-capabilities.zwave-alarm-v1-lock") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua new file mode 100644 index 0000000000..06c5e2980e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,175 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local lock_utils = require "new_lock_utils" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" +} + +--- Default handler for alarm command class reports, these were largely OEM-defined +--- +--- This converts alarm V1 reports to correct lock events +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Alarm.Report +local function alarm_report_handler(driver, device, cmd) + local alarm_type = cmd.args.alarm_type + local event = nil + local credential_index = nil + if (cmd.args.alarm_level ~= nil) then + credential_index = cmd.args.alarm_level + end + if (alarm_type == 9 or alarm_type == 17) then + event = capabilities.lock.lock.unknown() + elseif (alarm_type == 16 or alarm_type == 19) then + event = capabilities.lock.lock.unlocked() + if (credential_index ~= nil) then + local user_id = nil + local credential = lock_utils.get_credential(device, credential_index) + if (credential ~= nil) then + user_id = credential.userIndex + end + event.data = { userIndex = user_id, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (credential_index ~= nil) then + local user_id = nil + local credential = lock_utils.get_credential(device, credential_index) + if (credential ~= nil) then + user_id = credential.userIndex + end + event.data = { userIndex = user_id, method = METHOD.KEYPAD} + end + elseif (alarm_type == 21) then + event = capabilities.lock.lock.locked() + if (cmd.args.alarm_level == 2) then + event["data"] = {method = METHOD.MANUAL} + else + event["data"] = {method = METHOD.KEYPAD} + end + elseif (alarm_type == 22) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.MANUAL} + elseif (alarm_type == 23) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 24) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 25) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 26) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 27) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 32) then + -- all credentials have been deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + end + lock_utils.send_events(device) + elseif (alarm_type == 33) then + -- credential has been deleted. + if lock_utils.get_credential(device, credential_index) ~= nil then + lock_utils.delete_credential(device, credential_index) + lock_utils.send_events(device) + end + elseif (alarm_type == 13 or alarm_type == 112) then + local command = device:get_field(lock_utils.COMMAND_NAME) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- create credential if not already present. + if lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + lock_utils.send_events(device) + end + elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then + -- update credential + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + lock_utils.send_events(device) + end + else + -- out-of-band update. Don't add if already in table. + if lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = lock_utils.get_available_user_index(device) + if new_user_index ~= nil then + lock_utils.create_user(device, nil, "guest", new_user_index) + lock_utils.add_credential(device, + new_user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + lock_utils.send_events(device) + else + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, lock_utils.STATUS_RESOURCE_EXHAUSTED) + end + end + end + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- adding credential failed since code already exists. + -- remove the created user if one got made. There is no associated credential. + local command = device:get_field(lock_utils.COMMAND_NAME) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + if active_credential ~= nil then lock_utils.delete_user(device, active_credential.userIndex) end + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, lock_utils.STATUS_DUPLICATE) + end + elseif (alarm_type == 130) then + -- batteries replaced + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 161) then + -- tamper alarm + event = capabilities.tamperAlert.tamper.detected() + elseif (alarm_type == 167) then + -- low battery + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 168) then + -- critical battery + event = capabilities.battery.battery(1) + elseif (alarm_type == 169) then + -- battery too low to operate + event = capabilities.battery.battery(0) + end + + if (event ~= nil) then + device:emit_event(event) + end +end + +local zwave_lock = { + zwave_handlers = { + [cc.ALARM] = { + [Alarm.REPORT] = alarm_report_handler + } + }, + NAME = "Z-Wave lock alarm V1", + can_handle = require("using-new-capabilities.zwave-alarm-v1-lock.can_handle") +} + +return zwave_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua new file mode 100644 index 0000000000..1fe9815bb9 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local capabilities = require "st.capabilities" + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local subdriver = require("using-old-capabilities") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua new file mode 100644 index 0000000000..9585e81840 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,109 @@ +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local init_handler = function(driver, device, event) + local constants = require "st.zwave.constants" + -- temp fix before this can be changed from being persisted in memory + device:set_field(constants.CODE_STATE, nil, { persist = true }) +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function update_codes(driver, device, cmd) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + local delay = 0 + -- args.codes is json + for name, code in pairs(cmd.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + -- code changed + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({ + user_identifier = code_slot, + user_code = code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + end) + delay = delay + 2.2 + else + -- code deleted + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + delay = delay + 2.2 + device.thread:call_with_delay(delay, function () + device:send(UserCode:Get({user_identifier = code_slot})) + end) + delay = delay + 2.2 + end + end + end +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function migrate(driver, device, cmd) + local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + local get_lock_codes = LockCodesDefaults.get_lock_codes + local lock_users = {} + local lock_credentials = {} + local lock_codes = get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index = 1, #ordered_codes do + local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] + table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 10) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 8) + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) +end + +local using_old_capabilities = { + supported_capabilities = { + capabilities.lock, + capabilities.lockCodes, + capabilities.battery, + capabilities.tamperAlert + }, + lifecycle_handlers = { + init = init_handler, + }, + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes, + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, + }, + sub_drivers = require("using-old-capabilities.sub_drivers"), + can_handle = require("using-old-capabilities.can_handle"), + NAME = "Using old capabilities" +} + +return using_old_capabilities diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua similarity index 58% rename from drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua index d8bcd5756e..37e617d1b0 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua @@ -1,12 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) +return function(opts, driver, device, cmd) local KEYWE_MFR = 0x037B if device.zwave_manufacturer_id == KEYWE_MFR then - return true, require("keywe-lock") + local subdriver = require("using-old-capabilities.keywe-lock") + return true, subdriver end return false end - -return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua similarity index 95% rename from drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua index a51af26e00..325db40bec 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -65,7 +64,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = require("keywe-lock.can_handle"), + can_handle = require("using-old-capabilities.keywe-lock.can_handle"), } return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua similarity index 57% rename from drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua index e9222cb8fb..858debb3b0 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua @@ -1,12 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_samsung_lock(opts, self, device, cmd, ...) +return function(opts, driver, device, cmd) local SAMSUNG_MFR = 0x022E if device.zwave_manufacturer_id == SAMSUNG_MFR then - return true, require("samsung-lock") + local subdriver = require("using-old-capabilities.samsung-lock") + return true, subdriver end return false end - -return can_handle_samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua similarity index 96% rename from drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua index b2f4f60975..c4beb47653 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -17,8 +16,6 @@ local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted - - local function get_ongoing_code_set(device) local code_id local code_state = device:get_field(constants.CODE_STATE) @@ -90,7 +87,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = require("samsung-lock.can_handle"), + can_handle = require("using-old-capabilities.samsung-lock.can_handle"), } return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua similarity index 57% rename from drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua index e9f3cfb84c..9e2ecc2062 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua @@ -1,12 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_schlage_lock(opts, self, device, cmd, ...) +return function(opts, driver, device, cmd) local SCHLAGE_MFR = 0x003B if device.zwave_manufacturer_id == SCHLAGE_MFR then - return true, require("schlage-lock") + local subdriver = require("using-old-capabilities.schlage-lock") + return true, subdriver end return false end - -return can_handle_schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua similarity index 98% rename from drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua index 6b22049beb..0b8d1fb0d3 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local constants = require "st.zwave.constants" @@ -172,7 +171,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = require("schlage-lock.can_handle"), + can_handle = require("using-old-capabilities.schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua new file mode 100644 index 0000000000..8ec6bb0d72 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("using-old-capabilities.zwave-alarm-v1-lock"), + lazy_load_if_possible("using-old-capabilities.schlage-lock"), + lazy_load_if_possible("using-old-capabilities.samsung-lock"), + lazy_load_if_possible("using-old-capabilities.keywe-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua similarity index 60% rename from drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua index 7bb54f23f2..ecaba34f90 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua @@ -1,11 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) +return function(opts, driver, device, cmd) if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then - return true, require("zwave-alarm-v1-lock") + local subdriver = require("using-old-capabilities.zwave-alarm-v1-lock") + return true, subdriver end return false end - -return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua similarity index 95% rename from drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua index d7c862f22a..3862976dbf 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright © 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -19,13 +18,6 @@ local METHOD = { COMMAND = "command", AUTO = "auto" } - ---- Determine whether the passed command is a V1 alarm command ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is smoke co alarm - --- Default handler for alarm command class reports, these were largely OEM-defined --- --- This converts alarm V1 reports to correct lock events @@ -146,7 +138,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = require("zwave-alarm-v1-lock.can_handle"), + can_handle = require("using-old-capabilities.zwave-alarm-v1-lock.can_handle") } return zwave_lock From 360c9d106beef9e598fa13ab81aa4514ae89be14 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Fri, 1 May 2026 11:47:30 -0500 Subject: [PATCH 2/7] Remove stale test accidentally included during rebase --- .../test/test_zwave_lock_code_migration.lua | 251 ------------------ 1 file changed, 251 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua deleted file mode 100644 index 1eb2d093e5..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ /dev/null @@ -1,251 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - --- Mock out globals -local test = require "integration_test" -local capabilities = require "st.capabilities" -local zw = require "st.zwave" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local t_utils = require "integration_test.utils" -local zw_test_utils = require "integration_test.zwave_test_utils" -local utils = require "st.utils" - -local mock_datastore = require "integration_test.mock_env_datastore" - -local json = require "dkjson" - -local zwave_lock_endpoints = { - { - command_classes = { - { value = zw.BATTERY }, - { value = zw.DOOR_LOCK }, - { value = zw.USER_CODE }, - { value = zw.NOTIFICATION } - } - } -} - -local lockCodes = { - ["1"] = "Zach", - ["2"] = "Steven" -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } -) - -local mock_device_no_data = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - data = {} - } -) - -local expect_reload_all_codes_messages = function(dev, lc) - test.socket.capability:__expect_send(dev:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode(lc), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send( UserCode:UsersNumberGet({}):build_test_tx(dev.id) ) - test.socket.capability:__expect_send(dev:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(dev.id) ) -end - -test.register_coroutine_test( - "Device added data lock codes population", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added without data should function", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - expect_reload_all_codes_messages(mock_device_no_data,{}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added shouldn't change the datastores", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added with no data should update the datastores", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - -- This should happen as the data is empty at this point - expect_reload_all_codes_messages(mock_device_no_data, {}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - test.socket.device_lifecycle():__queue_receive(mock_device_no_data:generate_info_changed( - { - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } - )) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - - -test.register_coroutine_test( - "Device added data lock codes population, should not reload all codes", - function() - test.timer.__create_and_queue_test_time_advance_timer(31, "oneshot") - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.wait_for_events() - test.mock_time.advance_time(35) - -- Nothing should happen - end, - { - min_api_version = 17 - } -) - -test.run_registered_tests() From f74699377b457fb9817df76a25754b1b37667687 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 1 May 2026 23:14:31 -0500 Subject: [PATCH 3/7] Restructure zwave-lock --- .../src/using-new-capabilities/init.lua | 568 ------------------ .../src/using-old-capabilities/init.lua | 413 ------------- drivers/SmartThings/zwave-lock/src/init.lua | 318 ++++++++-- .../keywe-lock/can_handle.lua | 7 +- .../keywe-lock/init.lua | 20 +- .../zwave-lock/src/lazy_load_subdriver.lua | 2 + .../can_handle.lua | 6 +- .../init.lua | 27 +- .../samsung-lock/can_handle.lua | 2 +- .../samsung-lock/init.lua | 7 +- .../schlage-lock/can_handle.lua | 2 +- .../schlage-lock/init.lua | 5 +- .../src/legacy-handlers/sub_drivers.lua | 10 + .../zwave-alarm-v1-lock/can_handle.lua | 2 +- .../zwave-alarm-v1-lock/init.lua | 6 +- .../can_handle.lua | 13 +- .../samsung-lock/init.lua | 6 +- .../src/schlage-lock/can_handle.lua | 16 + .../schlage-lock/init.lua | 12 +- .../zwave-lock/src/sub_drivers.lua | 7 +- .../zwave-lock/src/test/test_keywe_lock.lua | 8 +- .../test/test_keywe_lock_new_capabilities.lua | 3 +- .../zwave-lock/src/test/test_lock_battery.lua | 6 +- .../zwave-lock/src/test/test_samsung_lock.lua | 6 +- .../test_samsung_lock_new_capabilities.lua | 5 +- .../zwave-lock/src/test/test_schlage_lock.lua | 7 +- .../test_schlage_lock_new_capabilities.lua | 3 +- .../zwave-lock/src/test/test_zwave_lock.lua | 31 +- .../test_zwave_lock_code_slga_migration.lua | 8 +- .../test/test_zwave_lock_new_capabilities.lua | 3 +- .../src/using-new-capabilities/init.lua | 418 ------------- .../keywe-lock/can_handle.lua | 11 - .../samsung-lock/can_handle.lua | 11 - .../schlage-lock/can_handle.lua | 11 - .../using-new-capabilities/sub_drivers.lua | 11 - .../zwave-alarm-v1-lock/can_handle.lua | 10 - .../keywe-lock/init.lua | 70 --- .../using-old-capabilities/sub_drivers.lua | 11 - .../src/zwave-alarm-v1-lock/can_handle.lua | 15 + .../zwave-alarm-v1-lock/init.lua | 6 +- ...ew_lock_utils.lua => zwave_lock_utils.lua} | 111 +++- 41 files changed, 532 insertions(+), 1682 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => }/keywe-lock/can_handle.lua (58%) rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => }/keywe-lock/init.lua (71%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/can_handle.lua (79%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/init.lua (78%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/samsung-lock/can_handle.lua (79%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/samsung-lock/init.lua (96%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/schlage-lock/can_handle.lua (79%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/schlage-lock/init.lua (98%) create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/zwave-alarm-v1-lock/can_handle.lua (79%) rename drivers/SmartThings/zwave-lock/src/{using-old-capabilities => legacy-handlers}/zwave-alarm-v1-lock/init.lua (97%) rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => samsung-lock}/can_handle.lua (53%) rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => }/samsung-lock/init.lua (93%) create mode 100644 drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => }/schlage-lock/init.lua (89%) delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => }/zwave-alarm-v1-lock/init.lua (97%) rename drivers/SmartThings/zwave-lock/src/{new_lock_utils.lua => zwave_lock_utils.lua} (79%) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua deleted file mode 100644 index 8fff4ee861..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ /dev/null @@ -1,568 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Zigbee Driver utilities -local device_management = require "st.zigbee.device_management" -local log = require "log" -local utils = require "st.utils" - - --- Zigbee Spec Utils -local clusters = require "st.zigbee.zcl.clusters" -local Alarm = clusters.Alarms -local LockCluster = clusters.DoorLock -local PowerConfiguration = clusters.PowerConfiguration - --- Capabilities -local capabilities = require "st.capabilities" -local Battery = capabilities.battery -local Lock = capabilities.lock -local LockCredentials = capabilities.lockCredentials -local LockUsers = capabilities.lockUsers - --- Enums -local UserStatusEnum = LockCluster.types.DrlkUserStatus -local UserTypeEnum = LockCluster.types.DrlkUserType -local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode - -local socket = require "cosock.socket" -local lock_utils = require "new_lock_utils" - -local DELAY_LOCK_EVENT = "_delay_lock_event" -local MAX_DELAY = 10 - -local reload_all_codes = function(device) - -- starts at first user code index then iterates through all lock codes as they come in - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then - device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then - device:send(LockCluster.attributes.MinPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) - end - if (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfTotalUsersSupported:read(device)) - end - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then - device:set_field(lock_utils.CHECKING_CODE, 1) - end - - device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) -end - -local init = function(driver, device) - lock_utils.reload_tables(device) - device.thread:call_with_delay(15, function(d) - reload_all_codes(device) - end) -end - -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - - device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) - device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) - - device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) - device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) - - device.thread:call_with_delay(2, function(d) - reload_all_codes(device) - end) -end - -local add_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then - return - end - local available_index = lock_utils.get_available_user_index(device) - local status = lock_utils.STATUS_SUCCESS - if available_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - else - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) - lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) - end - - if status == lock_utils.STATUS_SUCCESS then - lock_utils.send_events(device, lock_utils.LOCK_USERS) - end - - lock_utils.clear_busy_state(device, status) -end - -local update_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then - return - end - - local user_name = command.args.userName - local user_type = command.args.userType - local user_index = tonumber(command.args.userIndex) - local current_users = lock_utils.get_users(device) - local status = lock_utils.STATUS_FAILURE - - for _, user in pairs(current_users) do - if user.userIndex == user_index then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index}) - user.userName = user_name - user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - status = lock_utils.STATUS_SUCCESS - break - end - end - - lock_utils.clear_busy_state(device, status) -end - -local delete_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then - return - end - local status = lock_utils.STATUS_SUCCESS - local user_index = tonumber(command.args.userIndex) - if lock_utils.get_user(device, user_index) ~= nil then - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) - end - - local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) - if associated_credential ~= nil then - -- if there is an associated credential with this user then delete the credential - -- this command also handles the user deletion - driver:inject_capability_command(device, { - capability = capabilities.lockCredentials.ID, - command = capabilities.lockCredentials.commands.deleteCredential.NAME, - args = { associated_credential.credentialIndex, "pin" }, - override_busy_check = true - }) - else - lock_utils.delete_user(device, user_index) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end - else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end -end - -local delete_all_users_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then - return - end - local status = lock_utils.STATUS_SUCCESS - local current_users = lock_utils.get_users(device) - - local delay = 0 - for _, user in pairs(current_users) do - device.thread:call_with_delay(delay, function() - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteUser.NAME, - args = {user.userIndex}, - override_busy_check = true - }) - end) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - -local add_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local user_index = tonumber(command.args.userIndex) - local user_type = command.args.userType - local credential_type = command.args.credentialType - local credential_data = command.args.credentialData - local status = lock_utils.STATUS_SUCCESS - - local credential_index = lock_utils.get_available_credential_index(device) - if credential_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then - status = lock_utils.STATUS_OCCUPIED - elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then - status = lock_utils.STATUS_FAILURE - end - - if user_index == 0 then - user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, user_type, user_index) - else - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - end - end - - if status == lock_utils.STATUS_SUCCESS then - -- set the pin code and then validate it was successful when the GetPINCode response is received. - -- the credential creation and events will also be handled in that response. - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) - device:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) - ) - device.thread:call_with_delay(4, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, status) - end -end - -local update_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credential_index = tonumber(command.args.credentialIndex) - local credential_data = command.args.credentialData - local credential = lock_utils.get_credential(device, credential_index) - - if credential ~= nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - device:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) - ) - device.thread:call_with_delay(4, function() - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) - end -end - -local delete_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then - return - end - - local credential_index = tonumber(command.args.credentialIndex) - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - end - - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) - device.thread:call_with_delay(2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) - end -end - -local delete_all_credentials_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credentials = lock_utils.get_credentials(device) - local status = lock_utils.STATUS_SUCCESS - local delay = 0 - for _, credential in pairs(credentials) do - local credential_index = tonumber(credential.credentialIndex) - device.thread:call_with_delay(delay, function() - device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) - end) - device.thread:call_with_delay(delay + 2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - -local max_code_length_handler = function(driver, device, value) - device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) -end - -local min_code_length_handler = function(driver, device, value) - device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) -end - -local max_codes_handler = function(driver, device, value) - device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) - device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) -end - -local get_pin_response_handler = function(driver, device, zb_mess) - local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) - local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) - local command = device:get_field(lock_utils.COMMAND_NAME) - local status = lock_utils.STATUS_SUCCESS - local emit_event = false - - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then - -- create credential if not already present. - if lock_utils.get_credential(device, credential_index) == nil then - lock_utils.add_credential(device, - active_credential.userIndex, - active_credential.credentialType, - credential_index) - emit_event = true - end - elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then - -- update credential - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) - emit_event = true - end - else - -- Called by reloading the codes. Don't add if already in table. - if lock_utils.get_credential(device, credential_index) == nil then - local new_user_index = lock_utils.get_available_user_index(device) - if new_user_index ~= nil then - lock_utils.create_user(device, nil, "guest", new_user_index) - lock_utils.add_credential(device, - new_user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_event = true - else - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - end - end - end - elseif zb_mess.body.zcl_body.user_status.value == UserStatusEnum.AVAILABLE and command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then - -- tried to add a code that already is in use. - -- remove the created user if one got made. There is no associated credential. - status = lock_utils.STATUS_DUPLICATE - lock_utils.delete_user(device, active_credential.userIndex) - else - if lock_utils.get_credential(device, credential_index) ~= nil then - -- Credential has been deleted. - lock_utils.delete_credential(device, credential_index) - emit_event = true - end - end - - if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then - -- the credential we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, - capabilities.lockCredentials.pinUsersSupported.NAME) - if (credential_index >= last_slot) then - device:set_field(lock_utils.CHECKING_CODE, nil) - emit_event = true - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) - end - end - - if emit_event then - lock_utils.send_events(device) - end - -- ignore handling the busy state for these commands, they are handled within their own handlers - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then - lock_utils.clear_busy_state(device, status) - end -end - -local programming_event_handler = function(driver, device, zb_mess) - local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) - local command = device:get_field(lock_utils.COMMAND_NAME) - local emit_events = false - - if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then - -- Master code updated - device:emit_event(capabilities.lockCredentials.commandResult( - {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, - { state_change = true, visibility = { displayed = true } } - )) - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then - if (zb_mess.body.zcl_body.user_id.value == 0xFF) then - -- All credentials deleted - for _, credential in pairs(lock_utils.get_credentials(device)) do - lock_utils.delete_credential(device, credential.credentialIndex) - emit_events = true - end - else - -- One credential deleted - if (lock_utils.get_credential(device, credential_index) ~= nil) then - lock_utils.delete_credential(device, credential_index) - emit_events = true - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - if lock_utils.get_credential(device, credential_index) == nil and command == nil then - local user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, "guest", user_index) - lock_utils.add_credential(device, - user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_events = true - end - end - end - - if emit_events then - lock_utils.send_events(device) - end -end - --- REMOVE THIS AFTER DONE WITH TESTING -local migrate = function(driver, device, value) - log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. - "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. - "\n --------------------------------- \n") -end - -local lock_operation_event_handler = function(driver, device, zb_rx) - local event_code = zb_rx.body.zcl_body.operation_event_code.value - local source = zb_rx.body.zcl_body.operation_event_source.value - local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" - local METHOD = { - [0] = "keypad", - [1] = "command", - [2] = "manual", - [3] = "rfid", - [4] = "fingerprint", - [5] = "bluetooth" - } - local STATUS = { - [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() - } - local event = STATUS[event_code] - if (event ~= nil) then - event["data"] = {} - if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or - event_code == OperationEventCode.SCHEDULE_LOCK or - event_code == OperationEventCode.SCHEDULE_UNLOCK - ) then - event.data.method = "auto" - else - event.data.method = METHOD[source] - end - if (source == 0 and device:supports_capability_by_id(capabilities.lockUsers.ID)) then --keypad - local code_id = zb_rx.body.zcl_body.user_id.value - local code_name = "Code " .. code_id - local user = lock_utils.get_user(device, code_id) - if user ~= nil then - code_name = user.userName - end - - event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } - end - - -- if this is an event corresponding to a recently-received attribute report, we - -- want to set our delay timer for future lock attribute report events - if device:get_latest_state( - device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), - capabilities.lock.ID, - capabilities.lock.lock.ID) == event.value.value then - local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 - local time_diff = socket.gettime() - preceding_event_time - if time_diff < MAX_DELAY then - device:set_field(DELAY_LOCK_EVENT, time_diff) - end - end - - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) - end -end - - -local new_capabilities_driver = { - NAME = "Lock Driver Using New Capabilities", - supported_capabilities = { - Lock, - LockCredentials, - LockUsers, - Battery, - }, - zigbee_handlers = { - cluster = { - [LockCluster.ID] = { - [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, - [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, - [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler, - } - }, - attr = { - [LockCluster.ID] = { - [LockCluster.attributes.MaxPINCodeLength.ID] = max_code_length_handler, - [LockCluster.attributes.MinPINCodeLength.ID] = min_code_length_handler, - [LockCluster.attributes.NumberOfPINUsersSupported.ID] = max_codes_handler, - } - } - }, - capability_handlers = { - [LockUsers.ID] = { - [LockUsers.commands.addUser.NAME] = add_user_handler, - [LockUsers.commands.updateUser.NAME] = update_user_handler, - [LockUsers.commands.deleteUser.NAME] = delete_user_handler, - [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, - }, - [LockCredentials.ID] = { - [LockCredentials.commands.addCredential.NAME] = add_credential_handler, - [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, - [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, - [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, - }, - - [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING - [capabilities.lockCodes.commands.migrate.NAME] = migrate, - }, - }, - sub_drivers = require("using-new-capabilities.sub_drivers"), - health_check = false, - lifecycle_handlers = { - init = init, - doConfigure = do_configure - }, - can_handle = require("using-new-capabilities.can_handle") -} - -return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua deleted file mode 100644 index f8fcae368f..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ /dev/null @@ -1,413 +0,0 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Zigbee Driver utilities -local device_management = require "st.zigbee.device_management" - --- Zigbee Spec Utils -local clusters = require "st.zigbee.zcl.clusters" -local Alarm = clusters.Alarms -local LockCluster = clusters.DoorLock -local PowerConfiguration = clusters.PowerConfiguration - --- Capabilities -local capabilities = require "st.capabilities" -local Battery = capabilities.battery -local Lock = capabilities.lock -local LockCodes = capabilities.lockCodes -local LockCredentials = capabilities.lockCredentials -local LockUsers = capabilities.lockUsers - --- Enums -local UserStatusEnum = LockCluster.types.DrlkUserStatus -local UserTypeEnum = LockCluster.types.DrlkUserType -local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode - -local socket = require "cosock.socket" -local lock_utils = require "lock_utils" - -local DELAY_LOCK_EVENT = "_delay_lock_event" -local MAX_DELAY = 10 - -local reload_all_codes = function(driver, device, command) - -- starts at first user code index then iterates through all lock codes as they come in - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MinPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) - end - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end - device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) - device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) -end - -local get_pin_response_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = { codeName = lock_utils.get_code_name(device, code_slot) } - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - -- Code slot is occupied - event.value = code_slot .. lock_utils.get_change_type(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = event.data.codeName - device:emit_event(event) - lock_utils.lock_codes_event(device, lock_codes) - lock_utils.reset_code_state(device, code_slot) - else - -- Code slot is unoccupied - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - -- Code has been deleted - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - else - -- Code is unset - event.value = code_slot .. " unset" - device:emit_event(event) - end - end - - code_slot = tonumber(code_slot) - if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then - -- the code we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 - if (code_slot >= last_slot) then - device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) - device:set_field(lock_utils.CHECKING_CODE, nil) - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) - end - end -end - -local programming_event_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = {} - if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then - -- Master code changed - event.value = "0 set" - event.data = { codeName = "Master Code" } - device:emit_event(event) - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then - if (zb_mess.body.zcl_body.user_id.value == 0xFF) then - -- All codes deleted - for cs, _ in pairs(lock_utils.get_lock_codes(device)) do - lock_utils.code_deleted(device, cs) - end - lock_utils.lock_codes_event(device, {}) - else - -- One code deleted - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - -- Code added or changed - local change_type = lock_utils.get_change_type(device, code_slot) - local code_name = lock_utils.get_code_name(device, code_slot) - event.value = code_slot .. change_type - event.data = { codeName = code_name } - device:emit_event(event) - if (change_type == " set") then - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = code_name - lock_utils.lock_codes_event(device, lock_codes) - end - end -end - -local handle_max_codes = function(driver, device, value) - if value.value ~= 0 then - -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, - -- but it gave us a non-zero number of pin users, so we want to switch the profile - if not device:supports_capability_by_id(LockCodes.ID) then - device:try_update_metadata({ profile = "base-lock" }) -- switch to a lock with codes - lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes - if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes - -- so we'll load them manually - driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end - end - device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) - end -end - -local handle_max_code_length = function(driver, device, value) - device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) -end - -local handle_min_code_length = function(driver, device, value) - device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) -end - -local lock_operation_event_handler = function(driver, device, zb_rx) - local event_code = zb_rx.body.zcl_body.operation_event_code.value - local source = zb_rx.body.zcl_body.operation_event_source.value - local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" - local METHOD = { - [0] = "keypad", - [1] = "command", - [2] = "manual", - [3] = "rfid", - [4] = "fingerprint", - [5] = "bluetooth" - } - local STATUS = { - [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() - } - local event = STATUS[event_code] - if (event ~= nil) then - event["data"] = {} - if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or - event_code == OperationEventCode.SCHEDULE_LOCK or - event_code == OperationEventCode.SCHEDULE_UNLOCK - ) then - event.data.method = "auto" - else - event.data.method = METHOD[source] - end - if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad - local code_id = zb_rx.body.zcl_body.user_id.value - local code_name = "Code " .. code_id - local lock_codes = device:get_field("lockCodes") - if (lock_codes ~= nil and - lock_codes[code_id] ~= nil) then - code_name = lock_codes[code_id] - end - event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } - end - - -- if this is an event corresponding to a recently-received attribute report, we - -- want to set our delay timer for future lock attribute report events - if device:get_latest_state( - device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), - capabilities.lock.ID, - capabilities.lock.lock.ID) == event.value.value then - local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 - local time_diff = socket.gettime() - preceding_event_time - if time_diff < MAX_DELAY then - device:set_field(DELAY_LOCK_EVENT, time_diff) - end - end - - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) - end -end - -local update_codes = function(driver, device, command) - local delay = 0 - -- args.codes is json - for name, code in pairs(command.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - device.thread:call_with_delay(delay, function() - device:send(LockCluster.server.commands.SetPINCode(device, - code_slot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - code)) - end) - delay = delay + 2 - else - device.thread:call_with_delay(delay, function() - device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) - end) - delay = delay + 2 - end - device.thread:call_with_delay(delay, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) - end) - delay = delay + 2 - end - end -end - -local delete_code = function(driver, device, command) - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) - device.thread:call_with_delay(2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) - end) -end - -local request_code = function(driver, device, command) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) -end - -local set_code = function(driver, device, command) - if (command.args.codePIN == "") then - driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = { command.args.codeSlot, command.args.codeName } - }) - else - device:send(LockCluster.server.commands.SetPINCode(device, - command.args.codeSlot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - command.args.codePIN) - ) - if (command.args.codeName ~= nil) then - -- wait for confirmation from the lock to commit this to memory - -- Groovy driver has a lot more info passed here as a description string, may need to be investigated - local codeState = device:get_field(lock_utils.CODE_STATE) or {} - codeState["setName" .. command.args.codeSlot] = command.args.codeName - device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) - end - - device.thread:call_with_delay(4, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) - end) - end -end - -local name_slot = function(driver, device, command) - local code_slot = tostring(command.args.codeSlot) - local lock_codes = lock_utils.get_lock_codes(device) - if (lock_codes[code_slot] ~= nil) then - lock_codes[code_slot] = command.args.codeName - device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) - lock_utils.lock_codes_event(device, lock_codes) - end -end - -local migrate = function(driver, device, command) - local lock_users = {} - local lock_credentials = {} - local lock_codes = lock_utils.get_lock_codes(device) - local ordered_codes = {} - - for code in pairs(lock_codes) do - table.insert(ordered_codes, code) - end - - table.sort(ordered_codes) - for index, code_slot in ipairs(ordered_codes) do - table.insert(lock_users, { userIndex = index, userType = "guest", userName = lock_codes[code_slot] }) - table.insert(lock_credentials, { userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin" }) - end - - local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.minCodeLength.NAME, 4) - local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.maxCodeLength.NAME, 8) - local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 0) - if (code_length ~= nil) then - max_code_len = code_length - min_code_len = code_length - end - - device:emit_event(LockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - device:emit_event(LockUsers.users(lock_users, { visibility = { displayed = false } })) - device:emit_event(LockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) - device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } })) -end - -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - - device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) - device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) - - device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) - device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) - - -- Don't send a reload all codes if this is a part of migration - if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then - device.thread:call_with_delay(2, function(d) - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end) - else - device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) - end -end - -local old_capabilities_driver = { - NAME = "Lock Driver Using Old Capabilities", - supported_capabilities = { - Lock, - LockCodes, - Battery, - }, - zigbee_handlers = { - cluster = { - [LockCluster.ID] = { - [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, - [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, - [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler - } - }, - attr = { - [LockCluster.ID] = { - [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, - [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, - [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes, - } - } - }, - capability_handlers = { - [LockCodes.ID] = { - [LockCodes.commands.updateCodes.NAME] = update_codes, - [LockCodes.commands.deleteCode.NAME] = delete_code, - [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [LockCodes.commands.requestCode.NAME] = request_code, - [LockCodes.commands.setCode.NAME] = set_code, - [LockCodes.commands.nameSlot.NAME] = name_slot, - [LockCodes.commands.migrate.NAME] = migrate, - }, - }, - sub_drivers = { - require("using-old-capabilities.sub_drivers") - }, - health_check = false, - lifecycle_handlers = { - doConfigure = do_configure - }, - can_handle = require("using-old-capabilities.can_handle") -} - -return old_capabilities_driver diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 985daaa92e..db9b2bd7de 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,14 +1,39 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" +local LockUsers = capabilities.lockUsers +local LockCredentials = capabilities.lockCredentials +local lock_utils = require "zwave_lock_utils" +local utils = require "st.utils" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local log = require "log" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" --- @type st.zwave.Driver local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.defaults local defaults = require "st.zwave.defaults" +-- Helper methods +local reload_all_codes = function(device) + local max_codes = device:get_latest_state("main", + LockCredentials.ID, LockCredentials.pinUsersSupported.NAME) + if (max_codes == nil) then + device:send(UserCode:UsersNumberGet({})) + end + + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) + end + + device:send(UserCode:Get({user_identifier = device:get_field(lock_utils.CHECKING_CODE)})) +end + local do_refresh = function(self, device) local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) @@ -16,56 +41,237 @@ local do_refresh = function(self, device) device:send(Battery:Get({})) end -local SCAN_CODES_CHECK_INTERVAL = 30 - -local function periodic_codes_state_verification(driver, device) - local scan_codes_state = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME) - if scan_codes_state == "Scanning" then - driver:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - } - ) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(driver, device) - end - ) +-- Lifecycle handlers +local added_handler = function(driver, device) + if device:supports_capability_by_id(capabilities.lockCodes.ID) then + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + end + lock_utils.reload_tables(device) + device.thread:call_with_delay(2, function () + reload_all_codes(device) + end) + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) + if (device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) end + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) end -local do_added = function(driver, device) - -- this variable should only be present for test cases trying to test the old capabilities. - if device.useOldCapabilityForTesting == true then - -- added handler from using old capabilities - driver:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} }) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function() - periodic_codes_state_verification(driver, device) - end - ) - local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) - local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) - if (device:supports_capability(capabilities.tamperAlert)) then - device:emit_event(capabilities.tamperAlert.tamper.clear()) +local init_handler = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(10, function () + reload_all_codes(device) + end) +end + +-- Lock Users commands +local add_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then + return + end + local available_index = lock_utils.get_available_user_index(device) + local status = lock_utils.STATUS_SUCCESS + if available_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + else + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) + lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) + end + + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) + end + + lock_utils.clear_busy_state(device, status) +end + +local update_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then + return + end + + local user_name = command.args.userName + local user_type = command.args.userType + local user_index = tonumber(command.args.userIndex) + local current_users = lock_utils.get_users(device) + local status = lock_utils.STATUS_FAILURE + + for _, user in pairs(current_users) do + if user.userIndex == user_index then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break + end + end + + lock_utils.clear_busy_state(device, status) +end + +local delete_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then + return + end + local status = lock_utils.STATUS_SUCCESS + local user_index = tonumber(command.args.userIndex) + if lock_utils.get_user(device, user_index) ~= nil then + + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + end + + local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) + if associated_credential ~= nil then + -- if there is an associated credential with this user then delete the credential + -- this command also handles the user deletion + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { associated_credential.credentialIndex, "pin" }, + override_busy_check = true + }) + else + lock_utils.delete_user(device, user_index) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + lock_utils.clear_busy_state(device, status, command.override_busy_check) end else - if device:supports_capability_by_id(capabilities.lockCodes.ID) then - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - -- make the driver call this command again, it will now be handled in new capabilities. - driver.lifecycle_dispatcher:dispatch(driver, device, "added") + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end +end + +local delete_all_users_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then + return + end + local status = lock_utils.STATUS_SUCCESS + local current_users = lock_utils.get_users(device) + + local delay = 0 + for _, user in pairs(current_users) do + device.thread:call_with_delay(delay, function() + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = {user.userIndex}, + override_busy_check = true + }) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +--- Lock Credentials Commands + +local add_credential_handler = lock_utils.add_credential_handler + +local update_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credential_index = tonumber(command.args.credentialIndex) + local credential_data = command.args.credentialData + local credential = lock_utils.get_credential(device, credential_index) + + if credential ~= nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = credential_data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + -- clearing busy state handled in user_code_report_handler + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) + end +end + +local delete_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then + return + end + + local credential_index = tonumber(command.args.credentialIndex) + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) end + device:send(UserCode:Set({ + user_identifier = credential.credentialIndex, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + -- clearing busy state handled in user_code_report_handler + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) end end +local delete_all_credentials_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS + local delay = 0 + for _, credential in pairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) + device:send(UserCode:Set({ + user_identifier = credential_index, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +-- Z-Wave Message Handlers + +local user_code_report_handler = lock_utils.user_code_report_handler + +local notification_report_handler = function(driver, device, cmd) + ------------ USER CODE PROGRAMMING EVENTS ------------ + lock_utils.base_driver_code_event_handler(driver, device, cmd) + + ------------ LOCK OPERATION EVENTS ------------ + lock_utils.door_operation_event_handler(driver, device, cmd) + + ------------ TAMPER EVENTS ------------ + -- We have to load and call this manually since we're now overriding notfication handling + -- in this driver + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) +end + +local users_number_report_handler = function(driver, device, cmd) + -- these are the same for Z-Wave + device:emit_event(LockUsers.totalUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) +end + +-- Leave this here for logging purposes, it can be removed once lock migration is complete +local migrate = function(driver, device, value) + log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. + "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. + "\n --------------------------------- \n") +end + local function time_get_handler(driver, device, cmd) local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) local time = os.date("*t") @@ -89,16 +295,39 @@ local driver_template = { capabilities.tamperAlert }, lifecycle_handlers = { - added = do_added + added = added_handler, + init = init_handler, }, capability_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh - } + }, + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.migrate.NAME] = migrate, + }, }, zwave_handlers = { [cc.TIME] = { [0x01] = time_get_handler -- used by DanaLock + }, + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler, + [UserCode.USERS_NUMBER_REPORT] = users_number_report_handler, } }, sub_drivers = require("sub_drivers"), @@ -107,3 +336,4 @@ local driver_template = { defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) local lock = ZwaveDriver("zwave_lock", driver_template) lock:run() +return driver_template diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua similarity index 58% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua index 37e617d1b0..d8bcd5756e 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua @@ -1,11 +1,12 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -return function(opts, driver, device, cmd) +local function can_handle_keywe_lock(opts, self, device, cmd, ...) local KEYWE_MFR = 0x037B if device.zwave_manufacturer_id == KEYWE_MFR then - local subdriver = require("using-old-capabilities.keywe-lock") - return true, subdriver + return true, require("keywe-lock") end return false end + +return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua similarity index 71% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index 26873a9309..6081801c44 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -1,6 +1,7 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -9,7 +10,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local access_control_event = Notification.event.access_control local TamperDefaults = require "st.zwave.defaults.tamperAlert" -local lock_utils = require "new_lock_utils" local TAMPER_CLEAR_DELAY = 10 @@ -37,8 +37,18 @@ local function notification_report_handler(self, device, cmd) if event ~= nil then device:emit_event(event) else - lock_utils.door_operation_event_handler(self, device, cmd) - lock_utils.base_driver_code_event_handler(self, device, cmd) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local LockDefaults = require "st.zwave.defaults.lock" + local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + else + local lock_utils = require "zwave_lock_utils" + lock_utils.door_operation_event_handler(self, device, cmd) + lock_utils.base_driver_code_event_handler(self, device, cmd) + end + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) device.thread:call_with_delay( TAMPER_CLEAR_DELAY, @@ -63,7 +73,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = require("using-new-capabilities.keywe-lock.can_handle"), + can_handle = require("keywe-lock.can_handle"), } return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua index cadcf6c928..45115081e4 100644 --- a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -1,6 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + return function(sub_driver_name) -- gets the current lua libs api version local ZwaveDriver = require "st.zwave.driver" @@ -13,4 +14,5 @@ return function(sub_driver_name) else return require(sub_driver_name) end + end diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua similarity index 79% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua index 1fe9815bb9..92343f86e6 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua @@ -1,4 +1,4 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, ...) @@ -6,8 +6,8 @@ return function(opts, driver, device, ...) local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) if not lock_codes_migrated then - local subdriver = require("using-old-capabilities") + local subdriver = require("legacy-handlers") return true, subdriver end return false -end \ No newline at end of file +end diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua similarity index 78% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua index 9585e81840..6beacdef26 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua @@ -1,7 +1,12 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local init_handler = function(driver, device, event) local constants = require "st.zwave.constants" @@ -101,9 +106,23 @@ local using_old_capabilities = { [capabilities.lockCodes.commands.migrate.NAME] = migrate }, }, - sub_drivers = require("using-old-capabilities.sub_drivers"), - can_handle = require("using-old-capabilities.can_handle"), - NAME = "Using old capabilities" + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = function(driver, device, cmd) + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + local TamperDefaults = require "st.zwave.defaults.tamperAlert" + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + end + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT], + [UserCode.USERS_NUMBER_REPORT] = LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.USERS_NUMBER_REPORT], + } + }, + sub_drivers = require("legacy-handlers.sub_drivers"), + can_handle = require("legacy-handlers.can_handle"), + NAME = "legacy-handlers" } return using_old_capabilities diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua similarity index 79% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua index 858debb3b0..dc97d03ece 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua @@ -4,7 +4,7 @@ return function(opts, driver, device, cmd) local SAMSUNG_MFR = 0x022E if device.zwave_manufacturer_id == SAMSUNG_MFR then - local subdriver = require("using-old-capabilities.samsung-lock") + local subdriver = require("legacy-handlers.samsung-lock") return true, subdriver end return false diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua similarity index 96% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua index c4beb47653..6e28668b0b 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -16,6 +17,8 @@ local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted + + local function get_ongoing_code_set(device) local code_id local code_state = device:get_field(constants.CODE_STATE) @@ -87,7 +90,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = require("using-old-capabilities.samsung-lock.can_handle"), + can_handle = require("legacy-handlers.samsung-lock.can_handle"), } return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua similarity index 79% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua index 9e2ecc2062..e45491efe2 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua @@ -4,7 +4,7 @@ return function(opts, driver, device, cmd) local SCHLAGE_MFR = 0x003B if device.zwave_manufacturer_id == SCHLAGE_MFR then - local subdriver = require("using-old-capabilities.schlage-lock") + local subdriver = require("legacy-handlers.schlage-lock") return true, subdriver end return false diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua similarity index 98% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua index 0b8d1fb0d3..a6604b01f5 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local constants = require "st.zwave.constants" @@ -171,7 +172,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = require("using-old-capabilities.schlage-lock.can_handle"), + can_handle = require("legacy-handlers.schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua new file mode 100644 index 0000000000..058b0f41a9 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("legacy-handlers.zwave-alarm-v1-lock"), + lazy_load_if_possible("legacy-handlers.schlage-lock"), + lazy_load_if_possible("legacy-handlers.samsung-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua similarity index 79% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua index ecaba34f90..324f83e6f9 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua @@ -3,7 +3,7 @@ return function(opts, driver, device, cmd) if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then - local subdriver = require("using-old-capabilities.zwave-alarm-v1-lock") + local subdriver = require("legacy-handlers.zwave-alarm-v1-lock") return true, subdriver end return false diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua similarity index 97% rename from drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua index 3862976dbf..5865091852 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -18,6 +19,7 @@ local METHOD = { COMMAND = "command", AUTO = "auto" } + --- Default handler for alarm command class reports, these were largely OEM-defined --- --- This converts alarm V1 reports to correct lock events @@ -138,7 +140,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = require("using-old-capabilities.zwave-alarm-v1-lock.can_handle") + can_handle = require("legacy-handlers.zwave-alarm-v1-lock.can_handle") } return zwave_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua similarity index 53% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua rename to drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua index 5618b59b30..7e66818b2f 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -1,13 +1,16 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -return function(opts, driver, device, ...) +return function(opts, driver, device, cmd) local capabilities = require "st.capabilities" local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) if lock_codes_migrated then - local subdriver = require("using-new-capabilities") - return true, subdriver + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + local subdriver = require("samsung-lock") + return true, subdriver + end end return false -end \ No newline at end of file +end diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua similarity index 93% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua index 22f4c963eb..a2dfe24ef8 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -8,7 +8,7 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) local access_control_event = Notification.event.access_control -local lock_utils = require "new_lock_utils" +local lock_utils = require "zwave_lock_utils" local function notification_report_handler(self, device, cmd) local event @@ -58,7 +58,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = require("using-new-capabilities.samsung-lock.can_handle"), + can_handle = require("samsung-lock.can_handle"), } return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..16fc32e0b7 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local capabilities = require "st.capabilities" + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + local subdriver = require("schlage-lock") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua similarity index 89% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua index d303dbd466..83cc2f183f 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -11,7 +11,7 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2 local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) local Association = (require "st.zwave.CommandClass.Association")({version=1}) -local lock_utils = require "new_lock_utils" +local lock_utils = require "zwave_lock_utils" local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} @@ -70,8 +70,7 @@ local function user_code_report_handler(self, device, cmd) lock_utils.send_events(device) end else - local new_capabilities = require "using-new-capabilities" - new_capabilities.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + lock_utils.user_code_report_handler(self, device, cmd) end end @@ -79,8 +78,7 @@ local function add_credential_handler(self, device, cmd) local DEFAULT_COMMANDS_DELAY = 4.2 local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) local base_handler = function() - local new_capabilities = require "using-new-capabilities" - new_capabilities.capability_handlers[capabilities.lockCredentials.ID][capabilities.lockCredentials.commands.addCredential.NAME](self, device, cmd) + lock_utils.add_credential_handler(self, device, cmd) end if current_code_length == nil then device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) @@ -111,7 +109,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = require("using-new-capabilities.schlage-lock.can_handle"), + can_handle = require("schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua index 0e412d3da9..1558b7440a 100644 --- a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -3,8 +3,11 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("using-old-capabilities"), - lazy_load_if_possible("using-new-capabilities"), + lazy_load_if_possible("zwave-alarm-v1-lock"), + lazy_load_if_possible("schlage-lock"), + lazy_load_if_possible("samsung-lock"), + lazy_load_if_possible("keywe-lock"), lazy_load_if_possible("apiv6_bugfix"), + lazy_load_if_possible("legacy-handlers"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index 206e364669..a5c7aee7d4 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,10 +1,11 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) @@ -27,16 +28,13 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, zwave_product_type = KEYWE_PRODUCT_TYPE, - zwave_product_id = KEYWE_PRODUCT_ID, - useOldCapabilityForTesting = true, + zwave_product_id = KEYWE_PRODUCT_ID } ) --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end - test.set_test_init_function(test_init) test.register_coroutine_test( diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua index 463a968dad..f160a7780d 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" @@ -33,7 +33,6 @@ local mock_device = test.mock_device.build_test_zwave_device( } ) --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index e32fe5aeb2..11c03650c6 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -41,8 +42,7 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = DANALOCK_MANUFACTURER_ID, zwave_product_type = DANALOCK_PRODUCT_TYPE, - zwave_product_id = DANALOCK_PRODUCT_ID, - useOldCapabilityForTesting = true, + zwave_product_id = DANALOCK_PRODUCT_ID } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 437c1cb16a..81ba1df2ad 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local json = require "dkjson" @@ -19,8 +20,7 @@ local mock_device = test.mock_device.build_test_zwave_device( profile = t_utils.get_profile_definition("base-lock.yml"), zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID, - useOldCapabilityForTesting = true, + zwave_product_id = SAMSUNG_PRODUCT_ID } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua index ec68657690..7ee21d2e59 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" @@ -9,7 +9,7 @@ local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local lock_utils = require "new_lock_utils" +local lock_utils = require "zwave_lock_utils" local SAMSUNG_MANUFACTURER_ID = 0x022E local SAMSUNG_PRODUCT_TYPE = 0x0001 @@ -24,7 +24,6 @@ local mock_device = test.mock_device.build_test_zwave_device( } ) --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index 4f17c62bf7..38dabbd9dd 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -35,14 +36,12 @@ local mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID, - useOldCapabilityForTesting = true, + zwave_product_id = SCHLAGE_PRODUCT_ID } ) local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua index 34356601e9..fb203d2599 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" @@ -42,7 +42,6 @@ local mock_device = test.mock_device.build_test_zwave_device( local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index 9f26bbf7ef..d94fce2a1d 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,6 +1,7 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -34,8 +35,7 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - useOldCapabilityForTesting = true, + zwave_endpoints = zwave_lock_endpoints } ) @@ -53,31 +53,6 @@ local expect_reload_all_codes_messages = function() test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(mock_device.id) ) end -test.register_coroutine_test( - "When the device is added it should be set up and start reading codes", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - expect_reload_all_codes_messages() - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Door Lock Operation Reports should be handled", function() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua index 81946aa759..2cf67395a1 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -- Mock out globals @@ -30,8 +30,7 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - useOldCapabilityForTesting = true, + zwave_endpoints = zwave_lock_endpoints } ) @@ -41,8 +40,7 @@ local schlage_mock_device = test.mock_device.build_test_zwave_device( zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID, - useOldCapabilityForTesting = true, + zwave_product_id = SCHLAGE_PRODUCT_ID } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua index 73057ac7c8..b77de651f1 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua @@ -1,4 +1,4 @@ --- Copyright © 2022 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" @@ -96,7 +96,6 @@ local function add_credential(user_index) test_credential_index = test_credential_index + 1 end --- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua deleted file mode 100644 index e4241e9f03..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ /dev/null @@ -1,418 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" -local LockUsers = capabilities.lockUsers -local LockCredentials = capabilities.lockCredentials -local lock_utils = require "new_lock_utils" -local utils = require "st.utils" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) ---- @type st.zwave.CommandClass.Notification -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local log = require "log" -local TamperDefaults = require "st.zwave.defaults.tamperAlert" - --- Helper methods -local reload_all_codes = function(device) - local max_codes = device:get_latest_state("main", - LockCredentials.ID, LockCredentials.pinUsersSupported.NAME) - if (max_codes == nil) then - device:send(UserCode:UsersNumberGet({})) - end - - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then - device:set_field(lock_utils.CHECKING_CODE, 1) - end - - device:send(UserCode:Get({user_identifier = device:get_field(lock_utils.CHECKING_CODE)})) -end - --- Lifecycle handlers -local added_handler = function(driver, device) - lock_utils.reload_tables(device) - device.thread:call_with_delay(2, function () - reload_all_codes(device) - end) - -- read user/credential metadata - -- reload all codes - local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) - local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) - if (device:supports_capability(capabilities.tamperAlert)) then - device:emit_event(capabilities.tamperAlert.tamper.clear()) - end - device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) -end - -local init = function(driver, device) - lock_utils.reload_tables(device) - device.thread:call_with_delay(10, function () - reload_all_codes(device) - end) -end - --- Lock Users commands -local add_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then - return - end - local available_index = lock_utils.get_available_user_index(device) - local status = lock_utils.STATUS_SUCCESS - if available_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - else - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) - lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) - end - - if status == lock_utils.STATUS_SUCCESS then - lock_utils.send_events(device, lock_utils.LOCK_USERS) - end - - lock_utils.clear_busy_state(device, status) -end - -local update_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then - return - end - - local user_name = command.args.userName - local user_type = command.args.userType - local user_index = tonumber(command.args.userIndex) - local current_users = lock_utils.get_users(device) - local status = lock_utils.STATUS_FAILURE - - for _, user in pairs(current_users) do - if user.userIndex == user_index then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) - user.userName = user_name - user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - status = lock_utils.STATUS_SUCCESS - break - end - end - - lock_utils.clear_busy_state(device, status) -end - -local delete_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then - return - end - local status = lock_utils.STATUS_SUCCESS - local user_index = tonumber(command.args.userIndex) - if lock_utils.get_user(device, user_index) ~= nil then - - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) - end - - local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) - if associated_credential ~= nil then - -- if there is an associated credential with this user then delete the credential - -- this command also handles the user deletion - driver:inject_capability_command(device, { - capability = capabilities.lockCredentials.ID, - command = capabilities.lockCredentials.commands.deleteCredential.NAME, - args = { associated_credential.credentialIndex, "pin" }, - override_busy_check = true - }) - else - lock_utils.delete_user(device, user_index) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end - else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end -end - -local delete_all_users_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then - return - end - local status = lock_utils.STATUS_SUCCESS - local current_users = lock_utils.get_users(device) - - local delay = 0 - for _, user in pairs(current_users) do - device.thread:call_with_delay(delay, function() - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteUser.NAME, - args = {user.userIndex}, - override_busy_check = true - }) - end) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - ---- Lock Credentials Commands - -local add_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local user_index = tonumber(command.args.userIndex) - local user_type = command.args.userType - local credential_type = command.args.credentialType - local credential_data = command.args.credentialData - local status = lock_utils.STATUS_SUCCESS - - local credential_index = lock_utils.get_available_credential_index(device) - if credential_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then - status = lock_utils.STATUS_OCCUPIED - elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then - status = lock_utils.STATUS_FAILURE - end - - if user_index == 0 then - user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, user_type, user_index) - else - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - end - end - - if status == lock_utils.STATUS_SUCCESS then - -- set the pin code and then validate it was successful when the GetPINCode response is received. - -- the credential creation and events will also be handled in that response. - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) - - device:send(UserCode:Set({ - user_identifier = credential_index, - user_code = credential_data, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) - -- clearing busy state handled in user_code_report_handler - else - lock_utils.clear_busy_state(device, status) - end -end - -local update_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credential_index = tonumber(command.args.credentialIndex) - local credential_data = command.args.credentialData - local credential = lock_utils.get_credential(device, credential_index) - - if credential ~= nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - device:send(UserCode:Set({ - user_identifier = credential_index, - user_code = credential_data, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) - -- clearing busy state handled in user_code_report_handler - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) - end -end - -local delete_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then - return - end - - local credential_index = tonumber(command.args.credentialIndex) - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - end - device:send(UserCode:Set({ - user_identifier = credential.credentialIndex, - user_id_status = UserCode.user_id_status.AVAILABLE - })) - -- clearing busy state handled in user_code_report_handler - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) - end -end - -local delete_all_credentials_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credentials = lock_utils.get_credentials(device) - local status = lock_utils.STATUS_SUCCESS - local delay = 0 - for _, credential in pairs(credentials) do - local credential_index = tonumber(credential.credentialIndex) - device:send(UserCode:Set({ - user_identifier = credential_index, - user_id_status = UserCode.user_id_status.AVAILABLE - })) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - --- Z-Wave Message Handlers - -local user_code_report_handler = function(driver, device, cmd) - local credential_index = cmd.args.user_identifier - local command = device:get_field(lock_utils.COMMAND_NAME) - local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) - local user_id_status = cmd.args.user_id_status - local emit_events = false - - if (user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or - (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code)) then - -- credential exists on lock, add the credential if it doesn't exist in our table. - if lock_utils.get_credential(device, credential_index) == nil and command == nil then - local user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, "guest", user_index) - lock_utils.add_credential(device, - user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_events = true - end - elseif command ~= nil then - if command.name == lock_utils.ADD_CREDENTIAL and lock_utils.get_credential(device, credential_index) == nil then - lock_utils.add_credential(device, - active_credential.userIndex, - active_credential.credentialType, - credential_index) - emit_events = true - elseif command.name == lock_utils.UPDATE_CREDENTIAL then - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) - emit_events = true - end - end - end - elseif user_id_status == UserCode.user_id_status.AVAILABLE then - -- credential slot is open. If it exists on our table then remove it. - if lock_utils.get_credential(device, credential_index) ~= nil then - -- Credential has been deleted. - lock_utils.delete_credential(device, credential_index) - emit_events = true - end - end - - -- checking code handler - if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then - -- the credential we're checking has arrived - -- local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, - -- capabilities.lockCredentials.pinUsersSupported.NAME) - local last_slot = 8 -- remove this once testing is done - if (credential_index >= last_slot) then - device:set_field(lock_utils.CHECKING_CODE, nil) - emit_events = true - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(UserCode:Get({user_identifier = checkingCode})) - end - end - - if emit_events then - lock_utils.send_events(device) - end - - -- clear the busy state and handle the commandStatus - -- ignore handling the busy state for some commands, they are handled within their own handlers - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then - lock_utils.clear_busy_state(device, lock_utils.STATUS_SUCCESS) - end -end - -local notification_report_handler = function(driver, device, cmd) - ------------ USER CODE PROGRAMMING EVENTS ------------ - lock_utils.base_driver_code_event_handler(driver, device, cmd) - - ------------ LOCK OPERATION EVENTS ------------ - lock_utils.door_operation_event_handler(driver, device, cmd) - - ------------ TAMPER EVENTS ------------ - -- We have to load and call this manually since we're now overriding notfication handling - -- in this driver - TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) -end - -local users_number_report_handler = function(driver, device, cmd) - -- these are the same for Z-Wave - device:emit_event(LockUsers.totalUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) - device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) -end - --- REMOVE THIS AFTER DONE WITH TESTING -local migrate = function(driver, device, value) - log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. - "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. - "\n --------------------------------- \n") -end - -local zwave_lock = { - supported_capabilities = { - capabilities.lock, - capabilities.lockUsers, - capabilities.lockCredentials, - capabilities.battery, - capabilities.tamperAlert - }, - lifecycle_handlers = { - added = added_handler, - init = init, - }, - zwave_handlers = { - [cc.NOTIFICATION] = { - [Notification.REPORT] = notification_report_handler - }, - [cc.USER_CODE] = { - [UserCode.REPORT] = user_code_report_handler, - [UserCode.USERS_NUMBER_REPORT] = users_number_report_handler, - } - }, - capability_handlers = { - [LockUsers.ID] = { - [LockUsers.commands.addUser.NAME] = add_user_handler, - [LockUsers.commands.updateUser.NAME] = update_user_handler, - [LockUsers.commands.deleteUser.NAME] = delete_user_handler, - [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, - }, - [LockCredentials.ID] = { - [LockCredentials.commands.addCredential.NAME] = add_credential_handler, - [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, - [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, - [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, - }, - - [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING - [capabilities.lockCodes.commands.migrate.NAME] = migrate, - }, - }, - sub_drivers = require("using-new-capabilities.sub_drivers"), - NAME = "Using new capabilities", - can_handle = require("using-new-capabilities.can_handle") -} - -return zwave_lock \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua deleted file mode 100644 index 3b12277f18..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(opts, driver, device, cmd) - local KEYWE_MFR = 0x037B - if device.zwave_manufacturer_id == KEYWE_MFR then - local subdriver = require("using-new-capabilities.keywe-lock") - return true, subdriver - end - return false -end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua deleted file mode 100644 index d59b49e2e2..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(opts, driver, device, cmd) - local SAMSUNG_MFR = 0x022E - if device.zwave_manufacturer_id == SAMSUNG_MFR then - local subdriver = require("using-new-capabilities.samsung-lock") - return true, subdriver - end - return false -end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua deleted file mode 100644 index 4f1428dc5d..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(opts, driver, device, cmd) - local SCHLAGE_MFR = 0x003B - if device.zwave_manufacturer_id == SCHLAGE_MFR then - local subdriver = require("using-new-capabilities.schlage-lock") - return true, subdriver - end - return false -end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua deleted file mode 100644 index 4520fbf68f..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-new-capabilities.zwave-alarm-v1-lock"), - lazy_load_if_possible("using-new-capabilities.schlage-lock"), - lazy_load_if_possible("using-new-capabilities.samsung-lock"), - lazy_load_if_possible("using-new-capabilities.keywe-lock"), -} -return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua deleted file mode 100644 index f622dcf569..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua +++ /dev/null @@ -1,10 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(opts, driver, device, cmd) - if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then - local subdriver = require("using-new-capabilities.zwave-alarm-v1-lock") - return true, subdriver - end - return false -end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua deleted file mode 100644 index 325db40bec..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua +++ /dev/null @@ -1,70 +0,0 @@ --- Copyright © 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" -local cc = require "st.zwave.CommandClass" - -local Association = (require "st.zwave.CommandClass.Association")({version=2}) -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local access_control_event = Notification.event.access_control - -local LockDefaults = require "st.zwave.defaults.lock" -local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local TamperDefaults = require "st.zwave.defaults.tamperAlert" - -local TAMPER_CLEAR_DELAY = 10 - -local function clear_tamper_if_needed(device) - local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) - if current_tamper_state == "detected" then - device:emit_event(capabilities.tamperAlert.tamper.clear()) - end -end - -local function notification_report_handler(self, device, cmd) - local event - if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then - local event_code = cmd.args.event - if event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_OPEN then - event = capabilities.lock.lock.unlocked() - elseif event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_CLOSED then - event = capabilities.lock.lock.locked() - end - if event ~= nil then - event["data"] = {method = "manual"} - end - end - - if event ~= nil then - device:emit_event(event) - else - LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - device.thread:call_with_delay( - TAMPER_CLEAR_DELAY, - function(d) - clear_tamper_if_needed(device) - end - ) - end -end - -local function do_configure(self, device) - device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) -end - -local keywe_lock = { - zwave_handlers = { - [cc.NOTIFICATION] = { - [Notification.REPORT] = notification_report_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure - }, - NAME = "Keywe Lock", - can_handle = require("using-old-capabilities.keywe-lock.can_handle"), -} - -return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua deleted file mode 100644 index 8ec6bb0d72..0000000000 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-old-capabilities.zwave-alarm-v1-lock"), - lazy_load_if_possible("using-old-capabilities.schlage-lock"), - lazy_load_if_possible("using-old-capabilities.samsung-lock"), - lazy_load_if_possible("using-old-capabilities.keywe-lock"), -} -return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..0e95814fc4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local capabilities = require "st.capabilities" + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + local subdriver = require("zwave-alarm-v1-lock") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua similarity index 97% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua rename to drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua index 06c5e2980e..563fd93a99 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -8,7 +8,7 @@ local cc = require "st.zwave.CommandClass" local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) --- @type st.zwave.CommandClass.Battery local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local lock_utils = require "new_lock_utils" +local lock_utils = require "zwave_lock_utils" local METHOD = { KEYPAD = "keypad", @@ -169,7 +169,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = require("using-new-capabilities.zwave-alarm-v1-lock.can_handle") + can_handle = require("zwave-alarm-v1-lock.can_handle") } return zwave_lock diff --git a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua similarity index 79% rename from drivers/SmartThings/zwave-lock/src/new_lock_utils.lua rename to drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua index ea1c4c45c8..01e7e15364 100644 --- a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -35,6 +35,8 @@ local new_lock_utils = { USER_TYPE = "userType" } +local DEFAULT_SUPPORTED_PIN_SLOTS = 8 + -- check if we are currently busy performing a task. -- if we aren't then set as busy. new_lock_utils.busy_check_and_set = function (device, command, override_busy_check) @@ -138,7 +140,7 @@ end new_lock_utils.get_available_user_index = function(device) local max = device:get_latest_state("main", capabilities.lockUsers.ID, - capabilities.lockUsers.totalUsersSupported.NAME, 8) + capabilities.lockUsers.totalUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) local current_users = new_lock_utils.get_users(device) local available_index = nil local used_index = {} @@ -188,7 +190,7 @@ end new_lock_utils.get_available_credential_index = function(device) local max = device:get_latest_state("main", capabilities.lockCredentials.ID, - capabilities.lockCredentials.pinUsersSupported.NAME, 8) + capabilities.lockCredentials.pinUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) local current_credentials = new_lock_utils.get_credentials(device) local available_index = nil local used_index = {} @@ -486,4 +488,107 @@ new_lock_utils.door_operation_event_handler = function(driver, device, cmd) end end +function new_lock_utils.add_credential_handler(driver, device, command) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + if new_lock_utils.busy_check_and_set(device, {name = new_lock_utils.ADD_CREDENTIAL, type = new_lock_utils.LOCK_CREDENTIALS}) then + return + end + local user_index = tonumber(command.args.userIndex) + local user_type = command.args.userType + local credential_type = command.args.credentialType + local credential_data = command.args.credentialData + local status = new_lock_utils.STATUS_SUCCESS + + local credential_index = new_lock_utils.get_available_credential_index(device) + if credential_index == nil then + status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and new_lock_utils.get_credential_by_user_index(device, user_index) then + status = new_lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and new_lock_utils.get_user(device, user_index) == nil then + status = new_lock_utils.STATUS_FAILURE + end + + if user_index == 0 then + user_index = new_lock_utils.get_available_user_index(device) + if user_index ~= nil then + new_lock_utils.create_user(device, nil, user_type, user_index) + else + status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + + if status == new_lock_utils.STATUS_SUCCESS then + device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = credential_data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + -- clearing busy state handled in user_code_report_handler + else + new_lock_utils.clear_busy_state(device, status) + end +end + +function new_lock_utils.user_code_report_handler(driver, device, cmd) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + local credential_index = cmd.args.user_identifier + local command = device:get_field(new_lock_utils.COMMAND_NAME) + local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + local user_id_status = cmd.args.user_id_status + local emit_events = false + + if (user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or + (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code)) then + if new_lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = new_lock_utils.get_available_user_index(device) + if user_index ~= nil then + new_lock_utils.create_user(device, nil, "guest", user_index) + new_lock_utils.add_credential(device, user_index, new_lock_utils.CREDENTIAL_TYPE, credential_index) + emit_events = true + end + elseif command ~= nil then + if command.name == new_lock_utils.ADD_CREDENTIAL and new_lock_utils.get_credential(device, credential_index) == nil then + new_lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_events = true + elseif command.name == new_lock_utils.UPDATE_CREDENTIAL then + local credential = new_lock_utils.get_credential(device, credential_index) + if credential ~= nil then + new_lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_events = true + end + end + end + elseif user_id_status == UserCode.user_id_status.AVAILABLE then + if new_lock_utils.get_credential(device, credential_index) ~= nil then + new_lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + + if (credential_index == device:get_field(new_lock_utils.CHECKING_CODE)) then + local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) + if (credential_index >= last_slot) then + device:set_field(new_lock_utils.CHECKING_CODE, nil) + emit_events = true + else + local checkingCode = device:get_field(new_lock_utils.CHECKING_CODE) + 1 + device:set_field(new_lock_utils.CHECKING_CODE, checkingCode) + device:send(UserCode:Get({user_identifier = checkingCode})) + end + end + + if emit_events then + new_lock_utils.send_events(device) + end + + if command ~= nil and command ~= new_lock_utils.DELETE_ALL_CREDENTIALS and command ~= new_lock_utils.DELETE_ALL_USERS then + new_lock_utils.clear_busy_state(device, new_lock_utils.STATUS_SUCCESS) + end +end + return new_lock_utils From 7b30cc6c2a68993521990d1d5c35e553e84bacb2 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 4 May 2026 15:07:59 -0500 Subject: [PATCH 4/7] only update on added if state is typed --- drivers/SmartThings/zwave-lock/src/init.lua | 5 ++++- .../zwave-lock/src/test/test_keywe_lock_new_capabilities.lua | 3 +++ .../src/test/test_samsung_lock_new_capabilities.lua | 3 +++ .../src/test/test_schlage_lock_new_capabilities.lua | 3 +++ .../zwave-lock/src/test/test_zwave_lock_new_capabilities.lua | 2 ++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index db9b2bd7de..eca1b2c755 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -43,7 +43,10 @@ end -- Lifecycle handlers local added_handler = function(driver, device) - if device:supports_capability_by_id(capabilities.lockCodes.ID) then + if device:supports_capability_by_id(capabilities.lockCodes.ID) and device._provisioning_state == "TYPED" then + -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. + -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, + -- and should be migrated manually by the user. device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) end lock_utils.reload_tables(device) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua index f160a7780d..7c81f789ea 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua @@ -10,6 +10,8 @@ local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +test.disable_startup_messages() + local KEYWE_MANUFACTURER_ID = 0x037B local KEYWE_PRODUCT_TYPE = 0x0002 local KEYWE_PRODUCT_ID = 0x0001 @@ -26,6 +28,7 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, zwave_product_type = KEYWE_PRODUCT_TYPE, diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua index 7ee21d2e59..52a76d1f21 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua @@ -11,6 +11,8 @@ local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local lock_utils = require "zwave_lock_utils" +test.disable_startup_messages() + local SAMSUNG_MANUFACTURER_ID = 0x022E local SAMSUNG_PRODUCT_TYPE = 0x0001 local SAMSUNG_PRODUCT_ID = 0x0001 @@ -18,6 +20,7 @@ local SAMSUNG_PRODUCT_ID = 0x0001 local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, zwave_product_type = SAMSUNG_PRODUCT_TYPE, zwave_product_id = SAMSUNG_PRODUCT_ID, diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua index fb203d2599..992c5a4952 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua @@ -14,6 +14,8 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +test.disable_startup_messages() + local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 local SCHLAGE_PRODUCT_ID = 0x0469 @@ -33,6 +35,7 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua index b77de651f1..6efe2146c1 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua @@ -17,6 +17,7 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) local t_utils = require "integration_test.utils" local access_control_event = Notification.event.access_control +test.disable_startup_messages() -- supported comand classes local zwave_lock_endpoints = { @@ -36,6 +37,7 @@ local test_users = {} local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints } ) From 2a3bf848893dab3fb1e84de805129f9ff105cda3 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 6 May 2026 17:53:30 -0500 Subject: [PATCH 5/7] update test file names, add slga migration field for SOT --- drivers/SmartThings/zwave-lock/src/init.lua | 1 + .../zwave-lock/src/keywe-lock/init.lua | 6 +- .../src/legacy-handlers/can_handle.lua | 7 +- .../zwave-lock/src/legacy-handlers/init.lua | 2 + .../src/samsung-lock/can_handle.lua | 7 +- .../src/schlage-lock/can_handle.lua | 7 +- .../zwave-lock/src/test/test_keywe_lock.lua | 119 +- .../src/test/test_keywe_lock_legacy.lua | 101 ++ .../test/test_keywe_lock_new_capabilities.lua | 130 -- .../zwave-lock/src/test/test_samsung_lock.lua | 228 ++- .../src/test/test_samsung_lock_legacy.lua | 183 +++ .../test_samsung_lock_new_capabilities.lua | 265 ---- .../zwave-lock/src/test/test_schlage_lock.lua | 228 +-- ...ities.lua => test_schlage_lock_legacy.lua} | 226 ++- .../zwave-lock/src/test/test_zwave_lock.lua | 1284 ++++++++--------- .../test_zwave_lock_code_slga_migration.lua | 7 + .../src/test/test_zwave_lock_legacy.lua | 809 +++++++++++ .../test/test_zwave_lock_new_capabilities.lua | 731 ---------- .../src/zwave-alarm-v1-lock/can_handle.lua | 7 +- .../zwave-lock/src/zwave_lock_utils.lua | 3 +- 20 files changed, 2183 insertions(+), 2168 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua rename drivers/SmartThings/zwave-lock/src/test/{test_schlage_lock_new_capabilities.lua => test_schlage_lock_legacy.lua} (55%) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index eca1b2c755..1714a0147d 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -48,6 +48,7 @@ local added_handler = function(driver, device) -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, -- and should be migrated manually by the user. device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(lock_utils.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore end lock_utils.reload_tables(device) device.thread:call_with_delay(2, function () diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index 6081801c44..d563abe4d6 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -37,14 +37,14 @@ local function notification_report_handler(self, device, cmd) if event ~= nil then device:emit_event(event) else - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) - if not lock_codes_migrated then + local lock_utils = require "zwave_lock_utils" + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if not slga_migrated then local LockDefaults = require "st.zwave.defaults.lock" local LockCodesDefaults = require "st.zwave.defaults.lockCodes" LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) else - local lock_utils = require "zwave_lock_utils" lock_utils.door_operation_event_handler(self, device, cmd) lock_utils.base_driver_code_event_handler(self, device, cmd) end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua index 92343f86e6..3d69c340b7 100644 --- a/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, ...) - local capabilities = require "st.capabilities" - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.migrated.NAME, false) - if not lock_codes_migrated then + local lock_utils = require("zwave_lock_utils") + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) + if not slga_migrated then local subdriver = require("legacy-handlers") return true, subdriver end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua index 6beacdef26..37d3d255b4 100644 --- a/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua @@ -88,6 +88,8 @@ local function migrate(driver, device, cmd) device:emit_event(capabilities.lockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + local lock_utils = require("zwave_lock_utils") + device:set_field(lock_utils.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore end local using_old_capabilities = { diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua index 7e66818b2f..8cd03d2d9d 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, cmd) - local capabilities = require "st.capabilities" - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then + local lock_utils = require("zwave_lock_utils") + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) + if slga_migrated then local SAMSUNG_MFR = 0x022E if device.zwave_manufacturer_id == SAMSUNG_MFR then local subdriver = require("samsung-lock") diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua index 16fc32e0b7..d2ff9cb3e8 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, cmd) - local capabilities = require "st.capabilities" - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then + local lock_utils = require("zwave_lock_utils") + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) + if slga_migrated then local SCHLAGE_MFR = 0x003B if device.zwave_manufacturer_id == SCHLAGE_MFR then local subdriver = require("schlage-lock") diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index a5c7aee7d4..f5168d9c7b 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,13 +1,17 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local lock_utils = require "zwave_lock_utils" + +test.disable_startup_messages() local KEYWE_MANUFACTURER_ID = 0x037B local KEYWE_PRODUCT_TYPE = 0x0002 @@ -24,78 +28,105 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { - profile = t_utils.get_profile_definition("base-lock.yml"), + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, zwave_product_type = KEYWE_PRODUCT_TYPE, - zwave_product_id = KEYWE_PRODUCT_ID + zwave_product_id = KEYWE_PRODUCT_ID, } ) local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + test.register_coroutine_test( "Door Lock Operation Reports unlocked should be handled", function() + added() test.socket.zwave:__queue_receive({mock_device.id, DoorLock:OperationReport({door_lock_mode = 0x00}) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Door Lock Operation Reports locked should be handled", function() + added() test.socket.zwave:__queue_receive({mock_device.id, DoorLock:OperationReport({door_lock_mode = 0xFF}) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) - end, - { - min_api_version = 17 - } + end ) -test.register_message_test( +test.register_coroutine_test( "Lock notification reporting should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Notification:Report({notification_type = 6, event = 24}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Notification:Report({notification_type = 6, event = 25}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) - } - }, - { - min_api_version = 17 - } + function() + added() + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 24}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 25}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) + -- not a special case for this lock, should be handled as usual + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 6, event_parameter = "\x01"}) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua new file mode 100644 index 0000000000..a5c7aee7d4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua @@ -0,0 +1,101 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + + +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) + +local KEYWE_MANUFACTURER_ID = 0x037B +local KEYWE_PRODUCT_TYPE = 0x0002 +local KEYWE_PRODUCT_ID = 0x0001 + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = DoorLock} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, + zwave_product_type = KEYWE_PRODUCT_TYPE, + zwave_product_id = KEYWE_PRODUCT_ID + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Door Lock Operation Reports unlocked should be handled", + function() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0x00}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Door Lock Operation Reports locked should be handled", + function() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0xFF}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock notification reporting should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Notification:Report({notification_type = 6, event = 24}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Notification:Report({notification_type = 6, event = 25}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) + } + }, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua deleted file mode 100644 index 7c81f789ea..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua +++ /dev/null @@ -1,130 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) - -test.disable_startup_messages() - -local KEYWE_MANUFACTURER_ID = 0x037B -local KEYWE_PRODUCT_TYPE = 0x0002 -local KEYWE_PRODUCT_ID = 0x0001 - --- supported comand classes -local zwave_lock_endpoints = { - { - command_classes = { - {value = DoorLock} - } - } -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - _provisioning_state = "TYPED", - zwave_endpoints = zwave_lock_endpoints, - zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, - zwave_product_type = KEYWE_PRODUCT_TYPE, - zwave_product_id = KEYWE_PRODUCT_ID, - } -) - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) - test.wait_for_events() - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - for i = 1, 8 do - test.socket.zwave:__expect_send( - UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = i, - user_id_status = UserCode.user_id_status.AVAILABLE - })}) - end - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() -end - -test.register_coroutine_test( - "Door Lock Operation Reports unlocked should be handled", - function() - added() - test.socket.zwave:__queue_receive({mock_device.id, - DoorLock:OperationReport({door_lock_mode = 0x00}) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) - end -) - -test.register_coroutine_test( - "Door Lock Operation Reports locked should be handled", - function() - added() - test.socket.zwave:__queue_receive({mock_device.id, - DoorLock:OperationReport({door_lock_mode = 0xFF}) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) - end -) - -test.register_coroutine_test( - "Lock notification reporting should be handled", - function() - added() - test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 24}) } ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 25}) } ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) - -- not a special case for this lock, should be handled as usual - test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 6, event_parameter = "\x01"}) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 81ba1df2ad..05f901619f 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,15 +1,17 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" -local json = require "dkjson" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) +local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local constants = require "st.zwave.constants" +local lock_utils = require "zwave_lock_utils" + +test.disable_startup_messages() local SAMSUNG_MANUFACTURER_ID = 0x022E local SAMSUNG_PRODUCT_TYPE = 0x0001 @@ -18,47 +20,101 @@ local SAMSUNG_PRODUCT_ID = 0x0001 local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID + zwave_product_id = SAMSUNG_PRODUCT_ID, } ) local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + local function init_code_slot(slot_number, name, device) - local lock_codes = device.persistent_store[constants.LOCK_CODES] - if lock_codes == nil then - lock_codes = {} - device.persistent_store[constants.LOCK_CODES] = lock_codes + local credentials = device.transient_store[lock_utils.LOCK_CREDENTIALS] + local users = device.transient_store[lock_utils.LOCK_USERS] + if credentials == nil then + credentials = {} + device.transient_store[lock_utils.LOCK_CREDENTIALS] = credentials end - lock_codes[tostring(slot_number)] = name + if users == nil then + users = {} + device.transient_store[lock_utils.LOCK_USERS] = users + end + table.insert(credentials, { userIndex = slot_number, credentialIndex = slot_number, credentialType = "pin" }) + table.insert(users, { userIndex = slot_number, userName = name, userType = "guest" }) end test.register_coroutine_test( "When the device is added an unlocked event should be sent", function() + added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"}), { visibility = { displayed = false } })) - ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Setting a user code name should be handled", function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_device, @@ -89,95 +145,123 @@ test.register_coroutine_test( }) }) test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }) + ) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end ) test.register_coroutine_test( "Notification about correctly added code should be handled", function() - mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} - test.socket.zwave:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE - }) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true }))) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Notification about duplicated code should be handled", - function() - mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE }) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged(2 .. " failed", { state_change = true }))) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end ) test.register_coroutine_test( "All user codes should be reported as deleted upon changing Master Code", function() - init_code_slot(0, "Master Code", mock_device) + added() init_code_slot(1, "Code 1", mock_device) init_code_slot(2, "Code 2", mock_device) init_code_slot(3, "Code 3", mock_device) - test.socket.zwave:__queue_receive({ + test.socket.capability:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, - event_parameter = "" } - ) + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({ + { userIndex = 1, userName = "new name", userType = "guest" }, + { userIndex = 2, userName = "Code 2", userType = "guest" }, + { userIndex = 3, userName = "Code 3", userType = "guest" } + }, + { state_change = true, visibility = { displayed = true } }) ) ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, + event_parameter = "" } + ) + }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) ) ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua new file mode 100644 index 0000000000..81ba1df2ad --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua @@ -0,0 +1,183 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local json = require "dkjson" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local constants = require "st.zwave.constants" + +local SAMSUNG_MANUFACTURER_ID = 0x022E +local SAMSUNG_PRODUCT_TYPE = 0x0001 +local SAMSUNG_PRODUCT_ID = 0x0001 + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, + zwave_product_type = SAMSUNG_PRODUCT_TYPE, + zwave_product_id = SAMSUNG_PRODUCT_ID + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +test.register_coroutine_test( + "When the device is added an unlocked event should be sent", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"}), { visibility = { displayed = false } })) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = "" } + ) + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({user_identifier = 1}) + ) + ) + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Notification about correctly added code should be handled", + function() + mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + test.socket.zwave:__queue_receive({ mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + }) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Notification about duplicated code should be handled", + function() + mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + test.socket.zwave:__queue_receive({ mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + }) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged(2 .. " failed", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "All user codes should be reported as deleted upon changing Master Code", + function() + init_code_slot(0, "Master Code", mock_device) + init_code_slot(1, "Code 1", mock_device) + init_code_slot(2, "Code 2", mock_device) + init_code_slot(3, "Code 3", mock_device) + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, + event_parameter = "" } + ) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua deleted file mode 100644 index 52a76d1f21..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua +++ /dev/null @@ -1,265 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local capabilities = require "st.capabilities" -local zw_test_utils = require "integration_test.zwave_test_utils" -local t_utils = require "integration_test.utils" -local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) -local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local lock_utils = require "zwave_lock_utils" - -test.disable_startup_messages() - -local SAMSUNG_MANUFACTURER_ID = 0x022E -local SAMSUNG_PRODUCT_TYPE = 0x0001 -local SAMSUNG_PRODUCT_ID = 0x0001 - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, - zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID, - } -) - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) - test.wait_for_events() - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - for i = 1, 8 do - test.socket.zwave:__expect_send( - UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = i, - user_id_status = UserCode.user_id_status.AVAILABLE - })}) - end - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() -end - -local function init_code_slot(slot_number, name, device) - local credentials = device.transient_store[lock_utils.LOCK_CREDENTIALS] - local users = device.transient_store[lock_utils.LOCK_USERS] - if credentials == nil then - credentials = {} - device.transient_store[lock_utils.LOCK_CREDENTIALS] = credentials - end - if users == nil then - users = {} - device.transient_store[lock_utils.LOCK_USERS] = users - end - table.insert(credentials, { userIndex = slot_number, credentialIndex = slot_number, credentialType = "pin" }) - table.insert(users, { userIndex = slot_number, userName = name, userType = "guest" }) -end - -test.register_coroutine_test( - "When the device is added an unlocked event should be sent", - function() - added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Setting a user code name should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - event_parameter = "" } - ) - }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({user_identifier = 1}) - ) - ) - test.socket.zwave:__queue_receive({ - mock_device.id, - UserCode:Report({ - user_identifier = 1, - user_code = "1234", - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Notification about correctly added code should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "duplicate", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "All user codes should be reported as deleted upon changing Master Code", - function() - added() - init_code_slot(1, "Code 1", mock_device) - init_code_slot(2, "Code 2", mock_device) - init_code_slot(3, "Code 3", mock_device) - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "updateUser", - args = {1, "new name", "guest" } - }, - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "updateUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({ - { userIndex = 1, userName = "new name", userType = "guest" }, - { userIndex = 2, userName = "Code 2", userType = "guest" }, - { userIndex = 3, userName = "Code 3", userType = "guest" } - }, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, - event_parameter = "" } - ) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index 38dabbd9dd..4a6d1b0a77 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,18 +1,21 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" -local json = require "dkjson" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" +local lock_utils = require "zwave_lock_utils" local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + +test.disable_startup_messages() local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -33,10 +36,11 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID + zwave_product_id = SCHLAGE_PRODUCT_ID, } ) @@ -45,13 +49,64 @@ local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() +end + test.register_coroutine_test( "Setting a user code should result in the named code changed event firing", function() + added() test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_device, @@ -69,41 +124,36 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) - ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Setting a code length should be handled", - function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCodeLength", args = { 6 } } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Set({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6, - size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size - }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }) + ) ) - ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end ) test.register_coroutine_test( "Configuration report should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -112,17 +162,18 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + ) + end ) test.register_coroutine_test( "Configuration report indicating code deletion should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -131,8 +182,12 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) ) + test.wait_for_events() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -140,21 +195,34 @@ test.register_coroutine_test( configuration_value = 4 }) }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) + ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(4)) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) + ) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) + ) + end ) test.register_coroutine_test( "User code report indicating master code is available should indicate code deletion", - function () + function() + added() test.socket.zwave:__queue_receive({ mock_device.id, UserCode:Report({ @@ -162,55 +230,28 @@ test.register_coroutine_test( user_id_status = UserCode.user_id_status.AVAILABLE }) }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } }) + ) ) - end, - { - min_api_version = 17 - } -) - -local expect_reload_all_codes_messages = function() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:UsersNumberGet({}) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({ user_identifier = 1 }) - )) -end - -test.register_coroutine_test( - "Reload all codes should complete as expected", - function () - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {}, component = "main"} - }) - expect_reload_all_codes_messages() - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number - }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } }) ) ) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Device should send appropriate configuration messages", function() + added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( @@ -230,15 +271,13 @@ test.register_coroutine_test( ) ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Basic Sets should result in an Association remove", - function () + function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Basic:Set({ @@ -257,10 +296,7 @@ test.register_coroutine_test( }) ) ) - end, - { - min_api_version = 17 - } + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua similarity index 55% rename from drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua rename to drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua index 992c5a4952..38dabbd9dd 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua @@ -1,9 +1,11 @@ --- Copyright 2026 SmartThings, Inc. +-- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" +local json = require "dkjson" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" @@ -11,10 +13,6 @@ local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) - -test.disable_startup_messages() local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -35,11 +33,10 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID, + zwave_product_id = SCHLAGE_PRODUCT_ID } ) @@ -48,63 +45,13 @@ local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local function test_init() test.mock_device.add_test_device(mock_device) end - test.set_test_init_function(test_init) -local function added() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) - test.wait_for_events() - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - for i = 1, 8 do - test.socket.zwave:__expect_send( - UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = i, - user_id_status = UserCode.user_id_status.AVAILABLE - })}) - end - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() -end - test.register_coroutine_test( "Setting a user code should result in the named code changed event firing", function() - added() test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_device, @@ -122,36 +69,41 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = true } } - ) - ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a code length should be handled", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCodeLength", args = { 6 } } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + }) ) - end + ) + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( "Configuration report should be handled", function() - added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -160,18 +112,17 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) ) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( "Configuration report indicating code deletion should be handled", function() - added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -180,12 +131,8 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) ) - test.wait_for_events() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -193,34 +140,21 @@ test.register_coroutine_test( configuration_value = 4 }) }) - test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { state_change = true, visibility = { displayed = true } }) - ) + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) ) test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { state_change = true, visibility = { displayed = true } }) - ) + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(4)) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) - ) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( "User code report indicating master code is available should indicate code deletion", - function() - added() + function () test.socket.zwave:__queue_receive({ mock_device.id, UserCode:Report({ @@ -228,28 +162,55 @@ test.register_coroutine_test( user_id_status = UserCode.user_id_status.AVAILABLE }) }) - test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { state_change = true, visibility = { displayed = true } }) - ) + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { state_change = true, visibility = { displayed = true } }) + end, + { + min_api_version = 17 + } +) + +local expect_reload_all_codes_messages = function() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:UsersNumberGet({}) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({ user_identifier = 1 }) + )) +end + +test.register_coroutine_test( + "Reload all codes should complete as expected", + function () + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {}, component = "main"} + }) + expect_reload_all_codes_messages() + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number + }) ) ) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( "Device should send appropriate configuration messages", function() - added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( @@ -269,13 +230,15 @@ test.register_coroutine_test( ) ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( "Basic Sets should result in an Association remove", - function() - added() + function () test.socket.zwave:__queue_receive({ mock_device.id, Basic:Set({ @@ -294,7 +257,10 @@ test.register_coroutine_test( }) ) ) - end + end, + { + min_api_version = 17 + } ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index d94fce2a1d..a685d0cdd8 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,24 +1,24 @@ -- Copyright 2022 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" -local json = require "dkjson" ---- @type st.zwave.constants -local constants = require "st.zwave.constants" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) --- @type st.zwave.CommandClass.UserCode local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) local t_utils = require "integration_test.utils" -local zw_test_utils = require "integration_test.zwave_test_utils" +local access_control_event = Notification.event.access_control +local lock_utils = require "zwave_lock_utils" + +test.disable_startup_messages() -- supported comand classes local zwave_lock_endpoints = { @@ -31,446 +31,590 @@ local zwave_lock_endpoints = { } } } +local test_credential_index = 1 +local test_credentials = {} +local test_users = {} local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", + zwave_endpoints = zwave_lock_endpoints + } ) +-- if user_index is 0 it creates a new user. +local function add_credential(user_index) + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", "123" .. test_credential_index } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = test_credential_index, + user_code = "123" .. test_credential_index, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + local payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" + payload = payload:sub(1, 1) .. string.char(test_credential_index) .. payload:sub(3) + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = payload + }) + }) + table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(test_users, + { state_change = true, visibility = { displayed = true } }) + ) + ) + table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials(test_credentials, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + test_credential_index = test_credential_index + 1 +end + local function test_init() test.mock_device.add_test_device(mock_device) + + -- reset these globals + test_credential_index = 1 + test_credentials = {} + test_users = {} end + test.set_test_init_function(test_init) -local expect_reload_all_codes_messages = function() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send( UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(mock_device.id) ) +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + for i = 1, 8 do + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() end test.register_coroutine_test( - "Door Lock Operation Reports should be handled", + "Add user should succeed", function() - test.socket.zwave:__queue_receive({mock_device.id, - DoorLock:OperationReport({door_lock_mode = DoorLock.door_lock_mode.DOOR_SECURED}) + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) - end, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Battery percentage report should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, Battery:Report({ battery_level = 0x63 }) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(99)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Lock notification reporting should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.MANUAL_LOCK_OPERATION - }) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Code set reports should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, - UserCode:Report({ - user_identifier = 2, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"}), { visibility = { displayed = false } }) ) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", - { data = { codeName = "Code 2"}, state_change = true })) - } - }, - { - inner_block_ordering = "relaxed", - min_api_version = 17 - } -) - -test.register_message_test( - "Alarm tamper events should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.KEYPAD_TEMPORARY_DISABLED - }) - } - }, - - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Sending the lock command should be handled", - function() - test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({mock_device.id, - { capability = "lock", component = "main", command = "lock", args = {} } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, }) - test.socket.zwave:__expect_send(DoorLock:OperationSet({door_lock_mode = DoorLock.door_lock_mode.DOOR_SECURED}):build_test_tx(mock_device.id)) - test.wait_for_events() - test.mock_time.advance_time(4.2) - test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device.id)) - end, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Max user code number report should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, UserCode:UsersNumberReport({ supported_users = 16 }) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(16, { visibility = { displayed = false } })) - } - }, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end ) test.register_coroutine_test( - "Reloading all codes of an unconfigured lock should generate correct attribute checks", + "Add credential should succeed", function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) - expect_reload_all_codes_messages() - end, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Requesting a user code should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } - }, - { - channel = "zwave", - direction = "send", - message = UserCode:Get({user_identifier = 1}):build_test_tx(mock_device.id) - } - }, - { - min_api_version = 17 - } + added() + -- these all should succeed + add_credential(0) + add_credential(0) + add_credential(0) + end ) test.register_coroutine_test( - "Deleting a user code should be handled", + "Add credential for existing user should succeed", function() - test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", args = { 1 } } }) - test.socket.zwave:__expect_send(UserCode:Set( {user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE}):build_test_tx(mock_device.id)) + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "Guest1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Guest1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) test.wait_for_events() - test.mock_time.advance_time(4.2) - test.socket.zwave:__expect_send(UserCode:Get( {user_identifier = 1}):build_test_tx(mock_device.id)) - end, - { - min_api_version = 17 - } + -- add credential with the new users index (1). + add_credential(1) + end ) test.register_coroutine_test( - "Setting a user code should result in the named code changed event firing", + "Update user should succeed", function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id) ) + added() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) - ) - end, - { - min_api_version = 17 - } -) - -local function init_code_slot(slot_number, name, device) - local lock_codes = device.persistent_store[constants.LOCK_CODES] - if lock_codes == nil then - lock_codes = {} - device.persistent_store[constants.LOCK_CODES] = lock_codes + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "new name" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) end - lock_codes[tostring(slot_number)] = name -end +) test.register_coroutine_test( - "Setting a user code name should be handled", + "Delete user should succeed", function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"} ), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", - {state_change = true}))) - end, - { - min_api_version = 17 - } -) + added() + -- add credential + add_credential(0) -test.register_coroutine_test( - "Calling updateCodes should send properly spaced commands", - function () - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 2, user_code = "2345", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 3, user_code = "3456", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 4, user_id_status = UserCode.user_id_status.AVAILABLE}):build_test_tx(mock_device.id)) - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send(UserCode:Get({user_identifier = 4}):build_test_tx(mock_device.id)) + -- delete the user which should also delete the credential + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { 1 } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) test.wait_for_events() - end, - { - min_api_version = 17 - } -) -test.register_message_test( - "Master code programming event should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { mock_device.id, Notification:Report({ + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION - })} - }, - - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } + ) ) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "The lock reporting a single code has been set should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - UserCode:Report({ user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, user_identifier = 1}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }) ) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) - } - }, - { - inner_block_ordering = "relaxed", - min_api_version = 17 - } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + {}, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + end ) test.register_coroutine_test( - "The lock reporting a code has been deleted should be handled", + "Update credential should succeed", function() - init_code_slot(1, "Code 1", mock_device) - test.socket.zwave:__queue_receive( + added() + -- add credential + add_credential(0) + + -- update the credential + test.socket.capability:__queue_receive({mock_device.id, { - mock_device.id, - UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE}) - } + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { 1, 1, "pin", "3456" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "3456", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" -- update payload + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, { state_change = true, visibility = { displayed = true } }) + ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { state_change = true, visibility = { displayed = true } }) ) ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end ) test.register_coroutine_test( - "The lock reporting that all codes have been deleted should be handled", + "Delete credential should succeed", function() - init_code_slot(1, "Code 1", mock_device) - init_code_slot(2, "Code 2", mock_device) - init_code_slot(3, "Code 3", mock_device) - test.socket.zwave:__queue_receive( + added() + -- add the credential + add_credential(0) + + -- -- delete the credential + test.socket.capability:__queue_receive({mock_device.id, { - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.ALL_USER_CODES_DELETED - }) - } + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 1, "pin" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) ) + test.wait_for_events() - test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } + ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + {}, + { state_change = true, visibility = { displayed = true } }) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1, }, + { state_change = true, visibility = { displayed = true } } + ) ) ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } + test.wait_for_events() + end ) test.register_coroutine_test( - "The lock reporting unlock via code should include the code info in the report", + "Delete all users should succeed", function() - init_code_slot(1, "Superb Owl", mock_device) - test.socket.zwave:__queue_receive( + added() + -- add credential + add_credential(0) + -- add second credential + add_credential(0) + + -- delete all users. This should also delete the two associated credentials + test.socket.capability:__queue_receive({mock_device.id, { - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, - event_parameter = "" - }) - } + 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.mock_time.advance_time(0) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) ) + test.wait_for_events() + test.mock_time.advance_time(0.5) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + }) + }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Superb Owl" } }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 2, userName = "Guest2", userType = "guest" } + }, + { state_change = true, visibility = { displayed = true } }) ) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, + { state_change = true, visibility = { displayed = true } }) + ) + ) + + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success"}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.wait_for_events() + end ) test.register_coroutine_test( - "The lock reporting unlock via code should include the code number as the name if no name is set", + "The lock reporting unlock via code should include the code number", function() - init_code_slot(1, nil, mock_device) + added() + -- add credential + add_credential(0) + -- send unlock test.socket.zwave:__queue_receive( { mock_device.id, @@ -483,327 +627,107 @@ test.register_coroutine_test( ) test.socket.capability:__expect_send( mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + capabilities.lock.lock.unlocked({ data = { method = "keypad", userIndex = 1 } }) ) ) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( - "Getting all lock codes should advance as expected", + "Creating a credential should succeed if the lock responds with a user code report", function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) - expect_reload_all_codes_messages() + added() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 0, "guest", "pin", "123" .. test_credential_index } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = test_credential_index, + user_code = "123" .. test_credential_index, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) - for i = 1, 4 do - if (i ~= 1) then - test.socket.zwave:__expect_send(UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id)) - end - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = i, - user_id_status = UserCode.user_id_status.AVAILABLE - })}) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged(i.." unset", { state_change = true }) - ) - ) - end - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.scanCodes("Complete", { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } -) -test.register_message_test( - "Lock alarm reporting should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 22, alarm_level = 1}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 9}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 19, alarm_level = 3}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad", codeName = "Code 3", codeId="3"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 18, alarm_level=0}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad", codeName = "Master Code", codeId="0"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 21, alarm_level = 2}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 21, alarm_level = 1}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 23}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="command"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 24}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="command"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 25}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="command"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 26}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="auto"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 27}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="auto"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 32}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } })) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 13, alarm_level = 5}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["5"] = "Code 5"}), { visibility = { displayed = false } })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 set", {data={codeName="Code 5"}, state_change = true })) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 34, alarm_level = 2}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true })) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 161}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 168}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(1)) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Alarm:Report({alarm_type = 169}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Setting a user code should result in the named code changed event firing when notified via Notification CC", - function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id) ) + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + })}) + table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(test_users, + { state_change = true, visibility = { displayed = true } }) + ) + ) + table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials(test_credentials, + { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - v1_alarm_level = 1, - event_parameter = "" - }) }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) - ) - end, - { - min_api_version = 17 - } + test_credential_index = test_credential_index + 1 + end ) test.register_coroutine_test( - "When the device is added it should be set up and start reading codes", + "Lock alarm reporting should be handled", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { - min_api_version = 17 - } + added() + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 22, alarm_level = 1})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 9})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown())) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 19, alarm_level = 3})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 18, alarm_level=0})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 2})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 1})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 23})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 24})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 25})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="command"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 26})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="auto"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 27})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="auto"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 32})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 13, alarm_level = 5})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({ { userIndex = 1, userName = "Guest1", userType = "guest"} }, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials( { {userIndex = 1, credentialIndex = 5, credentialType = "pin" } }, { state_change = true, visibility = { displayed = true } }))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 34, alarm_level = 2})}) + -- no op because we have no active operation + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 161})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected())) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 168})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(1))) + test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 169})}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) + end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua index 2cf67395a1..a71b817037 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -11,6 +11,7 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version local t_utils = require "integration_test.utils" --- @type st.zwave.constants local constants = require "st.zwave.constants" +local lock_utils = require "zwave_lock_utils" local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -80,6 +81,8 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end ) @@ -95,6 +98,8 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(8, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end ) @@ -119,6 +124,8 @@ test.register_coroutine_test( test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(schlage_mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua new file mode 100644 index 0000000000..d94fce2a1d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua @@ -0,0 +1,809 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local json = require "dkjson" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +local t_utils = require "integration_test.utils" +local zw_test_utils = require "integration_test.zwave_test_utils" + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local expect_reload_all_codes_messages = function() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + test.socket.zwave:__expect_send( UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(mock_device.id) ) +end + +test.register_coroutine_test( + "Door Lock Operation Reports should be handled", + function() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = DoorLock.door_lock_mode.DOOR_SECURED}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, Battery:Report({ battery_level = 0x63 }) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(99)) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock notification reporting should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.MANUAL_LOCK_OPERATION + }) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Code set reports should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + UserCode:Report({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"}), { visibility = { displayed = false } }) ) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", + { data = { codeName = "Code 2"}, state_change = true })) + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 17 + } +) + +test.register_message_test( + "Alarm tamper events should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_TEMPORARY_DISABLED + }) + } + }, + + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Sending the lock command should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + test.socket.capability:__queue_receive({mock_device.id, + { capability = "lock", component = "main", command = "lock", args = {} } + }) + test.socket.zwave:__expect_send(DoorLock:OperationSet({door_lock_mode = DoorLock.door_lock_mode.DOOR_SECURED}):build_test_tx(mock_device.id)) + test.wait_for_events() + test.mock_time.advance_time(4.2) + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device.id)) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Max user code number report should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, UserCode:UsersNumberReport({ supported_users = 16 }) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(16, { visibility = { displayed = false } })) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Reloading all codes of an unconfigured lock should generate correct attribute checks", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Requesting a user code should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } + }, + { + channel = "zwave", + direction = "send", + message = UserCode:Get({user_identifier = 1}):build_test_tx(mock_device.id) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Deleting a user code should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", args = { 1 } } }) + test.socket.zwave:__expect_send(UserCode:Set( {user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE}):build_test_tx(mock_device.id)) + test.wait_for_events() + + test.mock_time.advance_time(4.2) + test.socket.zwave:__expect_send(UserCode:Get( {user_identifier = 1}):build_test_tx(mock_device.id)) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id) ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"} ), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", + {state_change = true}))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Calling updateCodes should send properly spaced commands", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zwave:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 2, user_code = "2345", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 3, user_code = "3456", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id)) + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 4, user_id_status = UserCode.user_id_status.AVAILABLE}):build_test_tx(mock_device.id)) + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:Get({user_identifier = 4}):build_test_tx(mock_device.id)) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Master code programming event should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION + })} + }, + + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "The lock reporting a single code has been set should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + UserCode:Report({ user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, user_identifier = 1}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }) ) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting a code has been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.zwave:__queue_receive( + { + mock_device.id, + UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE}) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting that all codes have been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + init_code_slot(2, "Code 2", mock_device) + init_code_slot(3, "Code 3", mock_device) + test.socket.zwave:__queue_receive( + { + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.ALL_USER_CODES_DELETED + }) + } + ) + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code info in the report", + function() + init_code_slot(1, "Superb Owl", mock_device) + test.socket.zwave:__queue_receive( + { + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = "" + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Superb Owl" } }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code number as the name if no name is set", + function() + init_code_slot(1, nil, mock_device) + test.socket.zwave:__queue_receive( + { + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = "\x01" + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Getting all lock codes should advance as expected", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + for i = 1, 4 do + if (i ~= 1) then + test.socket.zwave:__expect_send(UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id)) + end + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged(i.." unset", { state_change = true }) + ) + ) + end + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.scanCodes("Complete", { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock alarm reporting should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 22, alarm_level = 1}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 9}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 19, alarm_level = 3}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad", codeName = "Code 3", codeId="3"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 18, alarm_level=0}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad", codeName = "Master Code", codeId="0"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 21, alarm_level = 2}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 21, alarm_level = 1}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 23}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="command"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 24}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="command"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 25}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="command"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 26}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="auto"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 27}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="auto"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 32}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } })) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 13, alarm_level = 5}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["5"] = "Code 5"}), { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 set", {data={codeName="Code 5"}, state_change = true })) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 34, alarm_level = 2}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true })) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 161}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 168}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(1)) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Alarm:Report({alarm_type = 169}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing when notified via Notification CC", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id) ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + v1_alarm_level = 1, + event_parameter = "" + }) }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "When the device is added it should be set up and start reading codes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + DoorLock:OperationGet({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({}) + ) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua deleted file mode 100644 index 6efe2146c1..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua +++ /dev/null @@ -1,731 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local capabilities = require "st.capabilities" -local zw = require "st.zwave" ---- @type st.zwave.CommandClass.Notification -local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.Alarm -local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) -local t_utils = require "integration_test.utils" -local access_control_event = Notification.event.access_control - -test.disable_startup_messages() - --- supported comand classes -local zwave_lock_endpoints = { - { - command_classes = { - {value = zw.BATTERY}, - {value = zw.DOOR_LOCK}, - {value = zw.USER_CODE}, - {value = zw.NOTIFICATION} - } - } -} -local test_credential_index = 1 -local test_credentials = {} -local test_users = {} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - _provisioning_state = "TYPED", - zwave_endpoints = zwave_lock_endpoints - } -) - --- if user_index is 0 it creates a new user. -local function add_credential(user_index) - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { user_index, "guest", "pin", "123" .. test_credential_index } - }, - }) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = test_credential_index, - user_code = "123" .. test_credential_index, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - local payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" - payload = payload:sub(1, 1) .. string.char(test_credential_index) .. payload:sub(3) - test.socket.zwave:__queue_receive({mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.NEW_USER_CODE_ADDED, - payload = payload - }) - }) - table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users(test_users, - { state_change = true, visibility = { displayed = true } }) - ) - ) - table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials(test_credentials, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - test_credential_index = test_credential_index + 1 -end - -local function test_init() - test.mock_device.add_test_device(mock_device) - - -- reset these globals - test_credential_index = 1 - test_credentials = {} - test_users = {} -end - -test.set_test_init_function(test_init) - -local function added() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = {displayed = false}}))) - test.wait_for_events() - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - for i = 1, 8 do - test.socket.zwave:__expect_send( - UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = i, - user_id_status = UserCode.user_id_status.AVAILABLE - })}) - end - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() -end - -test.register_coroutine_test( - "Add user should succeed", - function() - added() - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "addUser", - args = { "TestUser 1", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "addUser", - args = { "TestUser 2", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 2 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Add credential should succeed", - function() - added() - -- these all should succeed - add_credential(0) - add_credential(0) - add_credential(0) - end -) - -test.register_coroutine_test( - "Add credential for existing user should succeed", - function() - added() - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "addUser", - args = { "Guest1", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Guest1" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - - -- add credential with the new users index (1). - add_credential(1) - end -) - -test.register_coroutine_test( - "Update user should succeed", - function() - added() - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "addUser", - args = { "TestUser 1", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "addUser", - args = { "TestUser 2", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 2 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "updateUser", - args = {1, "new name", "guest" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "new name" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "updateUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Delete user should succeed", - function() - added() - -- add credential - add_credential(0) - - -- delete the user which should also delete the credential - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "deleteUser", - args = { 1 } - }, - }) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.AVAILABLE - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - test.socket.zwave:__queue_receive({mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.SINGLE_USER_CODE_DELETED, - payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - {}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - end -) - -test.register_coroutine_test( - "Update credential should succeed", - function() - added() - -- add credential - add_credential(0) - - -- update the credential - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "updateCredential", - args = { 1, 1, "pin", "3456" } - }, - }) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 1, - user_code = "3456", - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - test.socket.zwave:__queue_receive({mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.NEW_USER_CODE_ADDED, - payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" -- update payload - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({{ userIndex = 1, userName = "Guest1", userType = "guest" }}, { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "updateCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Delete credential should succeed", - function() - added() - -- add the credential - add_credential(0) - - -- -- delete the credential - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "deleteCredential", - args = { 1, "pin" } - }, - }) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.AVAILABLE - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - test.socket.zwave:__queue_receive({mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.SINGLE_USER_CODE_DELETED, - payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - {}, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1, }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - end -) - -test.register_coroutine_test( - "Delete all users should succeed", - function() - added() - -- add credential - add_credential(0) - -- add second credential - add_credential(0) - - -- delete all users. This should also delete the two associated credentials - test.socket.capability:__queue_receive({mock_device.id, - { - 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.mock_time.advance_time(0) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.AVAILABLE - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - test.mock_time.advance_time(0.5) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 2, - user_id_status = UserCode.user_id_status.AVAILABLE - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - test.socket.zwave:__queue_receive({mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.SINGLE_USER_CODE_DELETED, - payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { - { userIndex = 2, userName = "Guest2", userType = "guest" } - }, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { - { userIndex = 2, credentialIndex = 2, credentialType = "pin" } - }, - { state_change = true, visibility = { displayed = true } }) - ) - ) - - - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "deleteAllUsers", statusCode = "success"}, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - end -) - -test.register_coroutine_test( - "The lock reporting unlock via code should include the code number", - function() - added() - -- add credential - add_credential(0) - -- send unlock - test.socket.zwave:__queue_receive( - { - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, - event_parameter = "\x01" - }) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked({ data = { method = "keypad", userIndex = 1 } }) - ) - ) - end -) - -test.register_coroutine_test( - "Creating a credential should succeed if the lock responds with a user code report", - function() - added() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 0, "guest", "pin", "123" .. test_credential_index } - }, - }) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = test_credential_index, - user_code = "123" .. test_credential_index, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }):build_test_tx(mock_device.id) - ) - test.wait_for_events() - - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - })}) - table.insert(test_users, { userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users(test_users, - { state_change = true, visibility = { displayed = true } }) - ) - ) - table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials(test_credentials, - { state_change = true, visibility = { displayed = true } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = test_credential_index }, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.wait_for_events() - test_credential_index = test_credential_index + 1 - end -) - -test.register_coroutine_test( - "Lock alarm reporting should be handled", - function() - added() - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 22, alarm_level = 1})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 9})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown())) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 19, alarm_level = 3})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 18, alarm_level=0})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 2})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 21, alarm_level = 1})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="keypad"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 23})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="command"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 24})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="command"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 25})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="command"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 26})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unknown({data={method="auto"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 27})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="auto"}}))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 32})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 13, alarm_level = 5})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockUsers.users({ { userIndex = 1, userName = "Guest1", userType = "guest"} }, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCredentials.credentials( { {userIndex = 1, credentialIndex = 5, credentialType = "pin" } }, { state_change = true, visibility = { displayed = true } }))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 34, alarm_level = 2})}) - -- no op because we have no active operation - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 161})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected())) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 168})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(1))) - test.socket.zwave:__queue_receive({ mock_device.id, Alarm:Report({alarm_type = 169})}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) - end -) - -test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua index 0e95814fc4..0d790992e0 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, cmd) - local capabilities = require "st.capabilities" - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then + local lock_utils = require("zwave_lock_utils") + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if slga_migrated then if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then local subdriver = require("zwave-alarm-v1-lock") return true, subdriver diff --git a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua index 01e7e15364..5a784ec4ce 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua @@ -32,7 +32,8 @@ local new_lock_utils = { UPDATE_USER = "updateUser", USER_INDEX = "userIndex", USER_NAME = "userName", - USER_TYPE = "userType" + USER_TYPE = "userType", + SLGA_MIGRATED = "slgaMigrated" } local DEFAULT_SUPPORTED_PIN_SLOTS = 8 From 5016cedc2da89c2f6a1b10df8c5d93680a8c77b9 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 7 May 2026 13:29:55 -0500 Subject: [PATCH 6/7] remove persistence on fields that are set on init --- drivers/SmartThings/zwave-lock/src/init.lua | 2 +- drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 1714a0147d..259b2cbcbc 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -108,7 +108,7 @@ local update_user_handler = function(driver, device, command) device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) user.userName = user_name user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + device:set_field(lock_utils.LOCK_USERS, current_users) lock_utils.send_events(device, lock_utils.LOCK_USERS) status = lock_utils.STATUS_SUCCESS break diff --git a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua index 5a784ec4ce..dce05b02be 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua @@ -218,7 +218,7 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) local current_users = new_lock_utils.get_users(device) table.insert(current_users, { userIndex = user_index, userType = user_type, userName = user_name }) - device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + device:set_field(new_lock_utils.LOCK_USERS, current_users) end new_lock_utils.delete_user = function(device, user_index) @@ -242,7 +242,7 @@ new_lock_utils.add_credential = function(device, user_index, credential_type, cr local credentials = new_lock_utils.get_credentials(device) table.insert(credentials, { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) return new_lock_utils.STATUS_SUCCESS end @@ -273,7 +273,7 @@ new_lock_utils.update_credential = function(device, credential_index, user_index if credential.credentialIndex == credential_index then credential.credentialType = credential_type credential.userIndex = user_index - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) status_code = new_lock_utils.STATUS_SUCCESS break end From f24f62f36a780bb81e384cd4362dc0bc2790751e Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 8 May 2026 10:45:31 -0500 Subject: [PATCH 7/7] fixup naming, fix failing command.name checks --- .../zwave-lock/src/samsung-lock/init.lua | 2 +- .../zwave-lock/src/test/test_zwave_lock.lua | 40 +- .../src/test/test_zwave_lock_credentials.lua | 632 ++++++++++++++++++ .../src/zwave-alarm-v1-lock/init.lua | 8 +- .../zwave-lock/src/zwave_lock_utils.lua | 266 ++++---- 5 files changed, 802 insertions(+), 146 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_credentials.lua diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua index a2dfe24ef8..66c86e3b6b 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -18,7 +18,7 @@ local function notification_report_handler(self, device, cmd) event = capabilities.lock.lock.unlocked() elseif event_code == access_control_event.NEW_USER_CODE_ADDED then local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) - local command = device:get_field(lock_utils.COMMAND_NAME) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL and active_credential ~= nil then device:send(UserCode:Get({ user_identifier = active_credential.credentialIndex })) return diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index a685d0cdd8..06fb34d81b 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -547,8 +547,10 @@ test.register_coroutine_test( }, }) + -- 3 timers: deleteUser(1) at delay=0, deleteUser(2) at delay=2, clear_busy_state at delay=8 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) test.socket.zwave:__expect_send( UserCode:Set({ @@ -566,20 +568,19 @@ test.register_coroutine_test( ) test.wait_for_events() + -- credential 1 deletion acknowledged: user 1 removed, user 2 and credential 2 still present test.socket.zwave:__queue_receive({mock_device.id, Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = access_control_event.SINGLE_USER_CODE_DELETED, - payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" -- delete payload + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" }) }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockUsers.users( - { - { userIndex = 2, userName = "Guest2", userType = "guest" } - }, + { { userIndex = 2, userName = "Guest2", userType = "guest" } }, { state_change = true, visibility = { displayed = true } }) ) ) @@ -587,14 +588,37 @@ test.register_coroutine_test( mock_device:generate_test_message( "main", capabilities.lockCredentials.credentials( - { - { userIndex = 2, credentialIndex = 2, credentialType = "pin" } - }, - { state_change = true, visibility = { displayed = true } }) + { { userIndex = 2, credentialIndex = 2, credentialType = "pin" } }, + { state_change = true, visibility = { displayed = true } }) ) ) + -- commandResult must NOT be emitted here; command is still in progress + test.wait_for_events() + -- credential 2 deletion acknowledged: both user 2 and credential 2 now removed + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x02\x00\xFF\x06\x0D\x00\x00" + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }) + ) + ) + test.wait_for_events() + -- final timer fires, emitting commandResult only after all operations complete + test.mock_time.advance_time(8) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_credentials.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_credentials.lua new file mode 100644 index 0000000000..8f1591301d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_credentials.lua @@ -0,0 +1,632 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local t_utils = require "integration_test.utils" +local access_control_event = Notification.event.access_control +local lock_utils = require "zwave_lock_utils" + +test.disable_startup_messages() + +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", + zwave_endpoints = zwave_lock_endpoints +}) + +-- Tracks expected users and credentials across test helpers +local test_credential_index +local test_credentials +local test_users + +local function test_init() + test.mock_device.add_test_device(mock_device) + test_credential_index = 1 + test_credentials = {} + test_users = {} +end + +test.set_test_init_function(test_init) + +-- Simulate the device being added (runs lifecycle, loads initial state) +local function added() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device.id)) + test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device.id)) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tamperAlert.tamper.clear())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device.id)) + for i = 1, 8 do + test.socket.zwave:__expect_send(UserCode:Get({user_identifier = i}):build_test_tx(mock_device.id)) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({ + user_identifier = i, + user_id_status = UserCode.user_id_status.AVAILABLE + })}) + end + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + test.wait_for_events() +end + +-- Helper: add a credential (user_index 0 = auto-create guest user). +-- Uses a Notification report (the primary confirmation path for add). +-- Updates test_users and test_credentials tracking tables. +local function add_credential(user_index) + local expected_user_index = (user_index == 0) and test_credential_index or user_index + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", "1234" } + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = test_credential_index, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + -- Notification confirms the code was added; v1_alarm_level encodes the credential index + local payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" + payload = payload:sub(1, 1) .. string.char(test_credential_index) .. payload:sub(3) + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = payload + }) + }) + + if user_index == 0 then + table.insert(test_users, { + userIndex = expected_user_index, + userName = "Guest" .. expected_user_index, + userType = "guest" + }) + end + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users(test_users, { state_change = true, visibility = { displayed = true } }))) + + table.insert(test_credentials, { + userIndex = expected_user_index, + credentialIndex = test_credential_index, + credentialType = "pin" + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(test_credentials, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", + credentialIndex = test_credential_index, userIndex = expected_user_index }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + test_credential_index = test_credential_index + 1 +end + +-- Helper: add a named user and return userIndex +local function add_user(user_name) + local user_index = #test_users + 1 + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { user_name, "guest" } + }}) + table.insert(test_users, { userIndex = user_index, userType = "guest", userName = user_name }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users(test_users, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = user_index }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + return user_index +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- addCredential tests +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "addCredential: auto-creates guest user and assigns sequential credential indices", + function() + added() + add_credential(0) -- credential 1, user 1 (Guest1) + add_credential(0) -- credential 2, user 2 (Guest2) + add_credential(0) -- credential 3, user 3 (Guest3) + -- All three should exist in state + assert(lock_utils.get_credential(mock_device, 1) ~= nil, "credential 1 should exist") + assert(lock_utils.get_credential(mock_device, 2) ~= nil, "credential 2 should exist") + assert(lock_utils.get_credential(mock_device, 3) ~= nil, "credential 3 should exist") + assert(lock_utils.get_user(mock_device, 1) ~= nil, "user 1 should exist") + assert(lock_utils.get_user(mock_device, 2) ~= nil, "user 2 should exist") + assert(lock_utils.get_user(mock_device, 3) ~= nil, "user 3 should exist") + end +) + +test.register_coroutine_test( + "addCredential: adding second credential for existing user returns STATUS_OCCUPIED", + function() + added() + local user_index = add_user("TestUser1") + -- add the first credential for this user, should succeed + add_credential(user_index) + + -- attempt to add a second credential for the same user (should fail with OCCUPIED) + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", "9999" } + }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "occupied" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + -- only one credential should be present for this user + local count = 0 + for _, cred in pairs(lock_utils.get_credentials(mock_device)) do + if cred.userIndex == user_index then count = count + 1 end + end + assert(count == 1, "user should have exactly one credential, got " .. count) + end +) + +test.register_coroutine_test( + "addCredential: adding credential for non-existent user returns STATUS_FAILURE", + function() + added() + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 99, "guest", "pin", "1234" } -- user 99 does not exist + }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- updateCredential tests +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "updateCredential: updates code in-place without creating a duplicate credential entry", + function() + added() + add_credential(0) -- credential 1, user 1 + + -- update credential 1 with a new pin + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { 1, 1, "pin", "9999" } + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "9999", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + -- lock acknowledges code update at index 1 (v1_alarm_level = 1) + local payload = "\x70\x01\x00\xFF\x06\x0E\x00\x00" -- v1_alarm_level byte = 1 + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + payload = payload + }) + }) + -- credential count must remain 1 (no duplicates) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users( + {{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + {{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", + credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + -- verify only one credential exists in driver state + local credentials = lock_utils.get_credentials(mock_device) + local count = 0 + for _ in pairs(credentials) do count = count + 1 end + assert(count == 1, "should have exactly 1 credential after update, got " .. count) + assert(credentials[1].credentialIndex == 1, "credential index should still be 1") + end +) + +test.register_coroutine_test( + "updateCredential: returns failure when credential does not exist", + function() + added() + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { 99, 1, "pin", "9999" } -- credential index 99 does not exist + }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- deleteCredential tests +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "deleteCredential: deletes the correct credential by credentialIndex", + function() + added() + add_credential(0) -- credential 1, user 1 + add_credential(0) -- credential 2, user 2 + + -- delete credential 1, leaving credential 2 intact + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 1, "pin" } + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + -- lock confirms deletion of credential index 1 (v1_alarm_level = 1) + local payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = payload + }) + }) + -- user 1 deleted, user 2 remains + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users( + {{ userIndex = 2, userName = "Guest2", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }))) + -- credential 1 deleted, credential 2 remains + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + {{ userIndex = 2, credentialIndex = 2, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", + credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + assert(lock_utils.get_credential(mock_device, 1) == nil, "credential 1 should be deleted") + assert(lock_utils.get_credential(mock_device, 2) ~= nil, "credential 2 should remain") + assert(lock_utils.get_user(mock_device, 1) == nil, "user 1 should be deleted") + assert(lock_utils.get_user(mock_device, 2) ~= nil, "user 2 should remain") + end +) + +test.register_coroutine_test( + "deleteCredential: deletes the correct credential when deleting the second of two", + function() + added() + add_credential(0) -- credential 1, user 1 + add_credential(0) -- credential 2, user 2 + + -- delete credential 2, leaving credential 1 intact + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 2, "pin" } + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + -- lock confirms deletion of credential index 2 (v1_alarm_level = 2) + local payload = "\x21\x02\x00\xFF\x06\x0D\x00\x00" + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = payload + }) + }) + -- user 2 deleted, user 1 remains + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users( + {{ userIndex = 1, userName = "Guest1", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }))) + -- credential 2 deleted, credential 1 remains + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + {{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", + credentialIndex = 2, userIndex = 2 }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + assert(lock_utils.get_credential(mock_device, 1) ~= nil, "credential 1 should remain") + assert(lock_utils.get_credential(mock_device, 2) == nil, "credential 2 should be deleted") + assert(lock_utils.get_user(mock_device, 1) ~= nil, "user 1 should remain") + assert(lock_utils.get_user(mock_device, 2) == nil, "user 2 should be deleted") + end +) + +test.register_coroutine_test( + "deleteCredential: also deletes the associated guest user", + function() + added() + add_credential(0) -- creates guest user 1 + credential 1 + + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 1, "pin" } + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + + local payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = payload + }) + }) + -- both user and credential lists should now be empty + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", + credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + assert(lock_utils.get_user(mock_device, 1) == nil, "associated user should also be deleted") + assert(lock_utils.get_credential(mock_device, 1) == nil, "credential should be deleted") + end +) + +test.register_coroutine_test( + "deleteCredential: returns failure for non-existent credential index", + function() + added() + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 99, "pin" } + }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- deleteAllCredentials tests +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "deleteAllCredentials: deletes all credentials and all associated users", + function() + added() + add_credential(0) -- credential 1, user 1 + add_credential(0) -- credential 2, user 2 + + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteAllCredentials", + args = {} + }}) + -- Both Z-wave Set commands are sent immediately (no timer delay between them) + test.socket.zwave:__expect_send( + UserCode:Set({ user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id)) + test.socket.zwave:__expect_send( + UserCode:Set({ user_identifier = 2, user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id)) + -- A clear_busy_state timer is set for (delay + 4) = (2+2+4) = 8 seconds + test.timer.__create_and_queue_test_time_advance_timer(8, "oneshot") + test.wait_for_events() + + -- Lock acknowledges deletion of credential 1 (v1_alarm_level = 1) + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x01\x00\xFF\x06\x0D\x00\x00" + }) + }) + -- user 1 and credential 1 deleted; user 2 and credential 2 still present + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users( + {{ userIndex = 2, userName = "Guest2", userType = "guest" }}, + { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + {{ userIndex = 2, credentialIndex = 2, credentialType = "pin" }}, + { state_change = true, visibility = { displayed = true } }))) + -- commandResult must NOT be emitted here (command is still in progress) + test.wait_for_events() + + -- Lock acknowledges deletion of credential 2 (v1_alarm_level = 2) + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + payload = "\x21\x02\x00\xFF\x06\x0D\x00\x00" + }) + }) + -- Now both user 2 and credential 2 are deleted + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + -- commandResult still must NOT be emitted here (timer hasn't fired yet) + test.wait_for_events() + + -- Timer fires -> commandResult emitted + test.mock_time.advance_time(8) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + + -- Verify final state: everything deleted + assert(lock_utils.get_credential(mock_device, 1) == nil, "credential 1 should be deleted") + assert(lock_utils.get_credential(mock_device, 2) == nil, "credential 2 should be deleted") + assert(lock_utils.get_user(mock_device, 1) == nil, "user 1 should be deleted") + assert(lock_utils.get_user(mock_device, 2) == nil, "user 2 should be deleted") + end +) + +test.register_coroutine_test( + "deleteAllCredentials: handles ALL_USER_CODES_DELETED notification from lock", + function() + added() + add_credential(0) -- credential 1, user 1 + add_credential(0) -- credential 2, user 2 + + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteAllCredentials", + args = {} + }}) + test.socket.zwave:__expect_send( + UserCode:Set({ user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id)) + test.socket.zwave:__expect_send( + UserCode:Set({ user_identifier = 2, user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id)) + test.timer.__create_and_queue_test_time_advance_timer(8, "oneshot") + test.wait_for_events() + + -- Some locks respond with ALL_USER_CODES_DELETED instead of individual events + -- ALL_USER_CODES_DELETED = 0x0C = 12 + test.socket.zwave:__queue_receive({mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED, + payload = "\x00\x00\x00\xFF\x06\x0C\x00\x00" + }) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } }))) + test.wait_for_events() + + test.mock_time.advance_time(8) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials: no-op when there are no credentials", + function() + added() + test.socket.capability:__queue_receive({mock_device.id, { + capability = capabilities.lockCredentials.ID, + command = "deleteAllCredentials", + args = {} + }}) + -- No Z-wave sends since there are no credentials + -- Timer fires immediately (delay = 0, so call_with_delay(4, ...)) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = true } } + ))) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua index 563fd93a99..ee1d92fcce 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua @@ -91,7 +91,7 @@ local function alarm_report_handler(driver, device, cmd) lock_utils.send_events(device) end elseif (alarm_type == 13 or alarm_type == 112) then - local command = device:get_field(lock_utils.COMMAND_NAME) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then -- create credential if not already present. @@ -121,7 +121,7 @@ local function alarm_report_handler(driver, device, cmd) credential_index) lock_utils.send_events(device) else - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + if command ~= nil and command.name ~= lock_utils.DELETE_ALL_CREDENTIALS and command.name ~= lock_utils.DELETE_ALL_USERS then lock_utils.clear_busy_state(device, lock_utils.STATUS_RESOURCE_EXHAUSTED) end end @@ -130,10 +130,10 @@ local function alarm_report_handler(driver, device, cmd) elseif (alarm_type == 34 or alarm_type == 113) then -- adding credential failed since code already exists. -- remove the created user if one got made. There is no associated credential. - local command = device:get_field(lock_utils.COMMAND_NAME) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) if active_credential ~= nil then lock_utils.delete_user(device, active_credential.userIndex) end - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + if command ~= nil and command.name ~= lock_utils.DELETE_ALL_CREDENTIALS and command.name ~= lock_utils.DELETE_ALL_USERS then lock_utils.clear_busy_state(device, lock_utils.STATUS_DUPLICATE) end elseif (alarm_type == 130) then diff --git a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua index dce05b02be..0c5e2c61c7 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave_lock_utils.lua @@ -5,12 +5,12 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local INITIAL_INDEX = 1 -local new_lock_utils = { +local lock_utils = { -- Constants ADD_CREDENTIAL = "addCredential", ADD_USER = "addUser", BUSY = "busy", - COMMAND_NAME = "commandName", + COMMAND_IN_PROGRESS = "commandInProgress", CREDENTIAL_TYPE = "pin", CHECKING_CODE = "checkingCode", DELETE_ALL_CREDENTIALS = "deleteAllCredentials", @@ -40,25 +40,25 @@ local DEFAULT_SUPPORTED_PIN_SLOTS = 8 -- check if we are currently busy performing a task. -- if we aren't then set as busy. -new_lock_utils.busy_check_and_set = function (device, command, override_busy_check) +lock_utils.busy_check_and_set = function (device, command, override_busy_check) if override_busy_check then -- the function was called by an injected command. return false end local c_time = os.time() - local busy_state = device:get_field(new_lock_utils.BUSY) or false + local busy_state = device:get_field(lock_utils.BUSY) or false if busy_state == false or c_time - busy_state > 10 then - device:set_field(new_lock_utils.COMMAND_NAME, command) - device:set_field(new_lock_utils.BUSY, c_time) + device:set_field(lock_utils.COMMAND_IN_PROGRESS, command) + device:set_field(lock_utils.BUSY, c_time) return false else local command_result_info = { commandName = command.name, - statusCode = new_lock_utils.STATUS_BUSY + statusCode = lock_utils.STATUS_BUSY } - if command.type == new_lock_utils.LOCK_USERS then + if command.type == lock_utils.LOCK_USERS then device:emit_event(capabilities.lockUsers.commandResult( command_result_info, { state_change = true, visibility = { displayed = true } } )) @@ -71,18 +71,18 @@ new_lock_utils.busy_check_and_set = function (device, command, override_busy_che end end -new_lock_utils.clear_busy_state = function(device, status, override_busy_check) +lock_utils.clear_busy_state = function(device, status, override_busy_check) if override_busy_check then return end - local command = device:get_field(new_lock_utils.COMMAND_NAME) - local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) if command ~= nil then local command_result_info = { commandName = command.name, statusCode = status } - if command.type == new_lock_utils.LOCK_USERS then + if command.type == lock_utils.LOCK_USERS then if active_credential ~= nil and active_credential.userIndex ~= nil then command_result_info.userIndex = active_credential.userIndex end @@ -102,35 +102,35 @@ new_lock_utils.clear_busy_state = function(device, status, override_busy_check) end end - device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, nil) - device:set_field(new_lock_utils.COMMAND_NAME, nil) - device:set_field(new_lock_utils.BUSY, false) + device:set_field(lock_utils.ACTIVE_CREDENTIAL, nil) + device:set_field(lock_utils.COMMAND_IN_PROGRESS, nil) + device:set_field(lock_utils.BUSY, false) end -new_lock_utils.reload_tables = function(device) +lock_utils.reload_tables = function(device) local users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.users.NAME, {}) local credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.credentials.NAME, {}) if next(users) ~= nil then - device:set_field(new_lock_utils.LOCK_USERS, users) + device:set_field(lock_utils.LOCK_USERS, users) end if next(credentials) ~= nil then - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) end - device:set_field(new_lock_utils.TABLES_LOADED, true) + device:set_field(lock_utils.TABLES_LOADED, true) end -new_lock_utils.get_users = function(device) - if not device:get_field(new_lock_utils.TABLES_LOADED) then - new_lock_utils.reload_tables(device) +lock_utils.get_users = function(device) + if not device:get_field(lock_utils.TABLES_LOADED) then + lock_utils.reload_tables(device) end - local users = utils.deep_copy(device:get_field(new_lock_utils.LOCK_USERS)) + local users = utils.deep_copy(device:get_field(lock_utils.LOCK_USERS)) return users ~= nil and users or {} end -new_lock_utils.get_user = function(device, user_index) - for _, user in pairs(new_lock_utils.get_users(device)) do +lock_utils.get_user = function(device, user_index) + for _, user in pairs(lock_utils.get_users(device)) do if user.userIndex == user_index then return user end @@ -139,10 +139,10 @@ new_lock_utils.get_user = function(device, user_index) return nil end -new_lock_utils.get_available_user_index = function(device) +lock_utils.get_available_user_index = function(device) local max = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) - local current_users = new_lock_utils.get_users(device) + local current_users = lock_utils.get_users(device) local available_index = nil local used_index = {} for _, user in pairs(current_users) do @@ -161,17 +161,17 @@ new_lock_utils.get_available_user_index = function(device) return available_index end -new_lock_utils.get_credentials = function(device) - if not device:get_field(new_lock_utils.TABLES_LOADED) then - new_lock_utils.reload_tables(device) +lock_utils.get_credentials = function(device) + if not device:get_field(lock_utils.TABLES_LOADED) then + lock_utils.reload_tables(device) end - local credentials = utils.deep_copy(device:get_field(new_lock_utils.LOCK_CREDENTIALS)) + local credentials = utils.deep_copy(device:get_field(lock_utils.LOCK_CREDENTIALS)) return credentials ~= nil and credentials or {} end -new_lock_utils.get_credential = function(device, credential_index) - for _, credential in pairs(new_lock_utils.get_credentials(device)) do +lock_utils.get_credential = function(device, credential_index) + for _, credential in pairs(lock_utils.get_credentials(device)) do if credential.credentialIndex == credential_index then return credential end @@ -179,8 +179,8 @@ new_lock_utils.get_credential = function(device, credential_index) return nil end -new_lock_utils.get_credential_by_user_index = function(device, user_index) - for _, credential in pairs(new_lock_utils.get_credentials(device)) do +lock_utils.get_credential_by_user_index = function(device, user_index) + for _, credential in pairs(lock_utils.get_credentials(device)) do if credential.userIndex == user_index then return credential end @@ -189,10 +189,10 @@ new_lock_utils.get_credential_by_user_index = function(device, user_index) return nil end -new_lock_utils.get_available_credential_index = function(device) +lock_utils.get_available_credential_index = function(device) local max = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) - local current_credentials = new_lock_utils.get_credentials(device) + local current_credentials = lock_utils.get_credentials(device) local available_index = nil local used_index = {} for _, credential in pairs(current_credentials) do @@ -211,53 +211,53 @@ new_lock_utils.get_available_credential_index = function(device) return available_index end -new_lock_utils.create_user = function(device, user_name, user_type, user_index) +lock_utils.create_user = function(device, user_name, user_type, user_index) if user_name == nil then user_name = "Guest" .. user_index end - local current_users = new_lock_utils.get_users(device) + local current_users = lock_utils.get_users(device) table.insert(current_users, { userIndex = user_index, userType = user_type, userName = user_name }) - device:set_field(new_lock_utils.LOCK_USERS, current_users) + device:set_field(lock_utils.LOCK_USERS, current_users) end -new_lock_utils.delete_user = function(device, user_index) - local current_users = new_lock_utils.get_users(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.delete_user = function(device, user_index) + local current_users = lock_utils.get_users(device) + local status_code = lock_utils.STATUS_FAILURE for index, user in pairs(current_users) do if user.userIndex == user_index then -- table.remove causes issues if we are removing while iterating. -- instead set the value as nil and let `prep_table` handle removing it. current_users[index] = nil - device:set_field(new_lock_utils.LOCK_USERS, current_users) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_USERS, current_users) + status_code = lock_utils.STATUS_SUCCESS break end end return status_code end -new_lock_utils.add_credential = function(device, user_index, credential_type, credential_index) - local credentials = new_lock_utils.get_credentials(device) +lock_utils.add_credential = function(device, user_index, credential_type, credential_index) + local credentials = lock_utils.get_credentials(device) table.insert(credentials, { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - return new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) + return lock_utils.STATUS_SUCCESS end -new_lock_utils.delete_credential = function(device, credential_index) - local credentials = new_lock_utils.get_credentials(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.delete_credential = function(device, credential_index) + local credentials = lock_utils.get_credentials(device) + local status_code = lock_utils.STATUS_FAILURE for index, credential in pairs(credentials) do if credential.credentialIndex == credential_index then - new_lock_utils.delete_user(device, credential.userIndex) + lock_utils.delete_user(device, credential.userIndex) -- table.remove causes issues if we are removing while iterating. -- instead set the value as nil and let `prep_table` handle removing it. credentials[index] = nil - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) + status_code = lock_utils.STATUS_SUCCESS break end end @@ -265,16 +265,16 @@ new_lock_utils.delete_credential = function(device, credential_index) return status_code end -new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) - local credentials = new_lock_utils.get_credentials(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = lock_utils.get_credentials(device) + local status_code = lock_utils.STATUS_FAILURE for _, credential in pairs(credentials) do if credential.credentialIndex == credential_index then credential.credentialType = credential_type credential.userIndex = user_index - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) + status_code = lock_utils.STATUS_SUCCESS break end end @@ -282,7 +282,7 @@ new_lock_utils.update_credential = function(device, credential_index, user_index end -- emit_event doesn't like having `nil` values in the table. Remove any if they are present. -new_lock_utils.prep_table = function(data) +lock_utils.prep_table = function(data) local clean_table = {} for _, value in pairs(data) do if value ~= nil then @@ -292,20 +292,20 @@ new_lock_utils.prep_table = function(data) return clean_table end -new_lock_utils.send_events = function(device, type) - if type == nil or type == new_lock_utils.LOCK_USERS then - local current_users = new_lock_utils.prep_table(new_lock_utils.get_users(device)) +lock_utils.send_events = function(device, type) + if type == nil or type == lock_utils.LOCK_USERS then + local current_users = lock_utils.prep_table(lock_utils.get_users(device)) device:emit_event(capabilities.lockUsers.users(current_users, {state_change = true, visibility = { displayed = true } })) end - if type == nil or type == new_lock_utils.LOCK_CREDENTIALS then - local credentials = new_lock_utils.prep_table(new_lock_utils.get_credentials(device)) + if type == nil or type == lock_utils.LOCK_CREDENTIALS then + local credentials = lock_utils.prep_table(lock_utils.get_credentials(device)) device:emit_event(capabilities.lockCredentials.credentials(credentials, { state_change = true, visibility = { displayed = true } })) end end -new_lock_utils.get_code_id_from_notification_event = function(event_params, v1_alarm_level) +lock_utils.get_code_id_from_notification_event = function(event_params, v1_alarm_level) -- some locks do not properly include the code ID in the event params, but do encode it -- in the v1 alarm level local code_id = v1_alarm_level @@ -318,88 +318,88 @@ end -- This is the part of the notifcation event handler code from the base driver -- that deals with lock code programming events -new_lock_utils.base_driver_code_event_handler = function(driver, device, cmd) +lock_utils.base_driver_code_event_handler = function(driver, device, cmd) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local access_control_event = Notification.event.access_control if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then local event = cmd.args.event - local credential_index = tonumber(new_lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level)) - local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) - local status = new_lock_utils.STATUS_SUCCESS - local command = device:get_field(new_lock_utils.COMMAND_NAME) + local credential_index = tonumber(lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level)) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local status = lock_utils.STATUS_SUCCESS + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local emit_event = false if (event == access_control_event.ALL_USER_CODES_DELETED) then -- all credentials have been deleted - for _, credential in pairs(new_lock_utils.get_credentials(device)) do - new_lock_utils.delete_credential(device, credential.credentialIndex) + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) emit_event = true end elseif (event == access_control_event.SINGLE_USER_CODE_DELETED) then -- credential has been deleted. - if new_lock_utils.get_credential(device, credential_index) ~= nil then - new_lock_utils.delete_credential(device, credential_index) + if lock_utils.get_credential(device, credential_index) ~= nil then + lock_utils.delete_credential(device, credential_index) emit_event = true end elseif (event == access_control_event.NEW_USER_CODE_ADDED) then - if command ~= nil and command.name == new_lock_utils.ADD_CREDENTIAL then + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then -- create credential if not already present. - if new_lock_utils.get_credential(device, credential_index) == nil then - new_lock_utils.add_credential(device, + if lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, active_credential.userIndex, active_credential.credentialType, credential_index) emit_event = true end - elseif command ~= nil and command.name == new_lock_utils.UPDATE_CREDENTIAL then + elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then -- update credential - local credential = new_lock_utils.get_credential(device, credential_index) + local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then - new_lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) emit_event = true end else -- out-of-band update. Don't add if already in table. - if new_lock_utils.get_credential(device, credential_index) == nil then - local new_user_index = new_lock_utils.get_available_user_index(device) + if lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = lock_utils.get_available_user_index(device) if new_user_index ~= nil then - new_lock_utils.create_user(device, nil, "guest", new_user_index) - new_lock_utils.add_credential(device, + lock_utils.create_user(device, nil, "guest", new_user_index) + lock_utils.add_credential(device, new_user_index, - new_lock_utils.CREDENTIAL_TYPE, + lock_utils.CREDENTIAL_TYPE, credential_index) emit_event = true else - status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + status = lock_utils.STATUS_RESOURCE_EXHAUSTED end end end elseif (event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE) then -- adding credential failed since code already exists. -- remove the created user if one got made. There is no associated credential. - status = new_lock_utils.STATUS_DUPLICATE - if active_credential ~= nil then new_lock_utils.delete_user(device, active_credential.userIndex) end + status = lock_utils.STATUS_DUPLICATE + if active_credential ~= nil then lock_utils.delete_user(device, active_credential.userIndex) end elseif (event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION) then -- master code changed -- should we send an index with this? device:emit_event(capabilities.lockCredentials.commandResult( - {commandName = new_lock_utils.UPDATE_CREDENTIAL, statusCode = new_lock_utils.STATUS_SUCCESS}, + {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, { state_change = true, visibility = { displayed = true } } )) end -- handle emitting events if any changes occured. if emit_event then - new_lock_utils.send_events(device) + lock_utils.send_events(device) end -- clear the busy state and handle the commandStatus -- ignore handling the busy state for some commands, they are handled within their own handlers - if command ~= nil and command ~= new_lock_utils.DELETE_ALL_CREDENTIALS and command ~= new_lock_utils.DELETE_ALL_USERS then - new_lock_utils.clear_busy_state(device, status) + if command ~= nil and command.name ~= lock_utils.DELETE_ALL_CREDENTIALS and command.name ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, status) end end end -new_lock_utils.door_operation_event_handler = function(driver, device, cmd) +lock_utils.door_operation_event_handler = function(driver, device, cmd) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local access_control_event = Notification.event.access_control if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then @@ -456,7 +456,7 @@ new_lock_utils.door_operation_event_handler = function(driver, device, cmd) code_id = (#event_params == 1) and event_params[1] or event_params[3] end local user_id = nil - local credential = new_lock_utils.get_credential(device, code_id) + local credential = lock_utils.get_credential(device, code_id) if (credential ~= nil) then user_id = credential.userIndex end @@ -489,37 +489,37 @@ new_lock_utils.door_operation_event_handler = function(driver, device, cmd) end end -function new_lock_utils.add_credential_handler(driver, device, command) +function lock_utils.add_credential_handler(driver, device, command) local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) - if new_lock_utils.busy_check_and_set(device, {name = new_lock_utils.ADD_CREDENTIAL, type = new_lock_utils.LOCK_CREDENTIALS}) then + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then return end local user_index = tonumber(command.args.userIndex) local user_type = command.args.userType local credential_type = command.args.credentialType local credential_data = command.args.credentialData - local status = new_lock_utils.STATUS_SUCCESS + local status = lock_utils.STATUS_SUCCESS - local credential_index = new_lock_utils.get_available_credential_index(device) + local credential_index = lock_utils.get_available_credential_index(device) if credential_index == nil then - status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED - elseif user_index ~= 0 and new_lock_utils.get_credential_by_user_index(device, user_index) then - status = new_lock_utils.STATUS_OCCUPIED - elseif user_index ~= 0 and new_lock_utils.get_user(device, user_index) == nil then - status = new_lock_utils.STATUS_FAILURE + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then + status = lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then + status = lock_utils.STATUS_FAILURE end if user_index == 0 then - user_index = new_lock_utils.get_available_user_index(device) + user_index = lock_utils.get_available_user_index(device) if user_index ~= nil then - new_lock_utils.create_user(device, nil, user_type, user_index) + lock_utils.create_user(device, nil, user_type, user_index) else - status = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + status = lock_utils.STATUS_RESOURCE_EXHAUSTED end end - if status == new_lock_utils.STATUS_SUCCESS then - device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, + if status == lock_utils.STATUS_SUCCESS then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) device:send(UserCode:Set({ user_identifier = credential_index, @@ -527,69 +527,69 @@ function new_lock_utils.add_credential_handler(driver, device, command) user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) -- clearing busy state handled in user_code_report_handler else - new_lock_utils.clear_busy_state(device, status) + lock_utils.clear_busy_state(device, status) end end -function new_lock_utils.user_code_report_handler(driver, device, cmd) +function lock_utils.user_code_report_handler(driver, device, cmd) local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) local credential_index = cmd.args.user_identifier - local command = device:get_field(new_lock_utils.COMMAND_NAME) - local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) local user_id_status = cmd.args.user_id_status local emit_events = false if (user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code)) then - if new_lock_utils.get_credential(device, credential_index) == nil and command == nil then - local user_index = new_lock_utils.get_available_user_index(device) + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) if user_index ~= nil then - new_lock_utils.create_user(device, nil, "guest", user_index) - new_lock_utils.add_credential(device, user_index, new_lock_utils.CREDENTIAL_TYPE, credential_index) + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, user_index, lock_utils.CREDENTIAL_TYPE, credential_index) emit_events = true end elseif command ~= nil then - if command.name == new_lock_utils.ADD_CREDENTIAL and new_lock_utils.get_credential(device, credential_index) == nil then - new_lock_utils.add_credential(device, + if command.name == lock_utils.ADD_CREDENTIAL and lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, active_credential.userIndex, active_credential.credentialType, credential_index) emit_events = true - elseif command.name == new_lock_utils.UPDATE_CREDENTIAL then - local credential = new_lock_utils.get_credential(device, credential_index) + elseif command.name == lock_utils.UPDATE_CREDENTIAL then + local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then - new_lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) emit_events = true end end end elseif user_id_status == UserCode.user_id_status.AVAILABLE then - if new_lock_utils.get_credential(device, credential_index) ~= nil then - new_lock_utils.delete_credential(device, credential_index) + if lock_utils.get_credential(device, credential_index) ~= nil then + lock_utils.delete_credential(device, credential_index) emit_events = true end end - if (credential_index == device:get_field(new_lock_utils.CHECKING_CODE)) then + if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, DEFAULT_SUPPORTED_PIN_SLOTS) if (credential_index >= last_slot) then - device:set_field(new_lock_utils.CHECKING_CODE, nil) + device:set_field(lock_utils.CHECKING_CODE, nil) emit_events = true else - local checkingCode = device:get_field(new_lock_utils.CHECKING_CODE) + 1 - device:set_field(new_lock_utils.CHECKING_CODE, checkingCode) + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) device:send(UserCode:Get({user_identifier = checkingCode})) end end if emit_events then - new_lock_utils.send_events(device) + lock_utils.send_events(device) end - if command ~= nil and command ~= new_lock_utils.DELETE_ALL_CREDENTIALS and command ~= new_lock_utils.DELETE_ALL_USERS then - new_lock_utils.clear_busy_state(device, new_lock_utils.STATUS_SUCCESS) + if command ~= nil and command.name ~= lock_utils.DELETE_ALL_CREDENTIALS and command.name ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, lock_utils.STATUS_SUCCESS) end end -return new_lock_utils +return lock_utils